Skip to content

Commit 9a97624

Browse files
authored
perf: cache measurements across layout param changes (#13)
1 parent faaab92 commit 9a97624

File tree

2 files changed

+126
-18
lines changed

2 files changed

+126
-18
lines changed

src/hooks/useLogoSoup.ts

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../constants";
1010
import type {
1111
LogoSource,
12+
MeasurementResult,
1213
NormalizedLogo,
1314
UseLogoSoupOptions,
1415
UseLogoSoupResult,
@@ -24,6 +25,12 @@ import {
2425
normalizeSource,
2526
} from "../utils/normalize";
2627

28+
interface CachedEntry {
29+
img: HTMLImageElement;
30+
measurement: MeasurementResult;
31+
blobUrl?: string;
32+
}
33+
2734
type State = {
2835
isLoading: boolean;
2936
normalizedLogos: NormalizedLogo[];
@@ -59,6 +66,22 @@ const INITIAL_STATE: State = {
5966
error: null,
6067
};
6168

69+
function clearCache(cache: Map<string, CachedEntry>) {
70+
for (const entry of cache.values()) {
71+
if (entry.blobUrl) URL.revokeObjectURL(entry.blobUrl);
72+
}
73+
cache.clear();
74+
}
75+
76+
function pruneCache(cache: Map<string, CachedEntry>, activeSrcs: Set<string>) {
77+
for (const [src, entry] of cache) {
78+
if (!activeSrcs.has(src)) {
79+
if (entry.blobUrl) URL.revokeObjectURL(entry.blobUrl);
80+
cache.delete(src);
81+
}
82+
}
83+
}
84+
6285
export function useLogoSoup(options: UseLogoSoupOptions): UseLogoSoupResult {
6386
const {
6487
logos,
@@ -78,48 +101,111 @@ export function useLogoSoup(options: UseLogoSoupOptions): UseLogoSoupResult {
78101
}
79102
const stableLogos = logosRef.current;
80103

104+
const cacheRef = useRef(new Map<string, CachedEntry>());
105+
const cacheKeyRef = useRef({
106+
contrastThreshold: NaN,
107+
densityAware: false,
108+
});
109+
110+
useEffect(() => {
111+
return () => clearCache(cacheRef.current);
112+
}, []);
113+
81114
useEffect(() => {
82115
if (stableLogos.length === 0) {
83116
dispatch({ type: "empty" });
84117
return;
85118
}
86119

87-
let cancelled = false;
88-
const blobUrls: string[] = [];
89-
dispatch({ type: "loading" });
120+
const cache = cacheRef.current;
121+
const prevKey = cacheKeyRef.current;
122+
123+
if (
124+
prevKey.contrastThreshold !== contrastThreshold ||
125+
prevKey.densityAware !== densityAware
126+
) {
127+
clearCache(cache);
128+
cacheKeyRef.current = { contrastThreshold, densityAware };
129+
}
90130

91131
const sources: LogoSource[] = stableLogos.map(normalizeSource);
132+
const activeSrcs = new Set(sources.map((s) => s.src));
133+
pruneCache(cache, activeSrcs);
134+
135+
const allCached = sources.every((s) => cache.has(s.src));
136+
const needsCrop =
137+
cropToContent &&
138+
sources.some((s) => {
139+
const entry = cache.get(s.src);
140+
return entry && !entry.blobUrl && entry.measurement.contentBox;
141+
});
142+
143+
if (allCached && !needsCrop) {
144+
const effectiveDensityFactor = densityAware ? densityFactor : 0;
145+
const results = sources.map((source) => {
146+
const entry = cache.get(source.src)!;
147+
const normalized = createNormalizedLogo(
148+
source,
149+
entry.measurement,
150+
baseSize,
151+
scaleFactor,
152+
effectiveDensityFactor,
153+
);
154+
if (cropToContent && entry.blobUrl) {
155+
normalized.croppedSrc = entry.blobUrl;
156+
}
157+
return normalized;
158+
});
159+
dispatch({ type: "success", normalizedLogos: results });
160+
return;
161+
}
162+
163+
let cancelled = false;
164+
if (!allCached) {
165+
dispatch({ type: "loading" });
166+
}
92167

93168
Promise.allSettled(
94169
sources.map(async (source) => {
95-
const img = await loadImage(source.src);
170+
let entry = cache.get(source.src);
96171

97-
if (cancelled) throw new Error("cancelled");
172+
if (!entry) {
173+
const img = await loadImage(source.src);
174+
if (cancelled) throw new Error("cancelled");
98175

99-
const measurement = measureWithContentDetection(
100-
img,
101-
contrastThreshold,
102-
densityAware,
103-
);
176+
const measurement = measureWithContentDetection(
177+
img,
178+
contrastThreshold,
179+
densityAware,
180+
);
181+
entry = { img, measurement };
182+
cache.set(source.src, entry);
183+
}
104184

105185
const effectiveDensityFactor = densityAware ? densityFactor : 0;
106186

107187
const normalized = createNormalizedLogo(
108188
source,
109-
measurement,
189+
entry.measurement,
110190
baseSize,
111191
scaleFactor,
112192
effectiveDensityFactor,
113193
);
114194

115-
if (cropToContent && measurement.contentBox) {
116-
const url = await cropToBlobUrl(img, measurement.contentBox);
195+
if (cropToContent && entry.measurement.contentBox && !entry.blobUrl) {
196+
const url = await cropToBlobUrl(
197+
entry.img,
198+
entry.measurement.contentBox,
199+
);
117200
if (cancelled) {
118201
URL.revokeObjectURL(url);
119202
throw new Error("cancelled");
120203
}
121-
blobUrls.push(url);
122-
normalized.croppedSrc = url;
204+
entry.blobUrl = url;
205+
}
206+
207+
if (cropToContent && entry.blobUrl) {
208+
normalized.croppedSrc = entry.blobUrl;
123209
}
124210

125211
return normalized;
@@ -150,9 +236,6 @@ export function useLogoSoup(options: UseLogoSoupOptions): UseLogoSoupResult {
150236

151237
return () => {
152238
cancelled = true;
153-
for (const url of blobUrls) {
154-
URL.revokeObjectURL(url);
155-
}
156239
};
157240
}, [
158241
stableLogos,

tests/bench/run.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,20 @@ const benchMount20Baseline = () => {
188188
}
189189
};
190190

191+
const benchReNormalize20 = () => {
192+
for (let i = 0; i < 20; i++) {
193+
const idx = i % allLogos.length;
194+
const logo = createNormalizedLogo(
195+
sources[idx]!,
196+
realMeasurements[idx]!,
197+
DEFAULT_BASE_SIZE,
198+
DEFAULT_SCALE_FACTOR,
199+
DEFAULT_DENSITY_FACTOR,
200+
);
201+
blackhole(getVisualCenterTransform(logo, DEFAULT_ALIGN_BY));
202+
}
203+
};
204+
191205
const keyBenchmarks: Record<string, () => void> = {
192206
"content detection (1 logo)": benchMeasure,
193207
"render pass (20 logos)": benchGetVCT20,
@@ -247,6 +261,17 @@ const abComparisons: ABComparison[] = [
247261
fn: () => {},
248262
},
249263
},
264+
{
265+
name: "layout update: full mount vs cached",
266+
a: {
267+
label: "full mount",
268+
fn: benchMount20,
269+
},
270+
b: {
271+
label: "cached re-normalize",
272+
fn: benchReNormalize20,
273+
},
274+
},
250275
];
251276

252277
console.log();

0 commit comments

Comments
 (0)