Skip to content

Commit 7d5fb2b

Browse files
committed
squash perf experiments
1 parent a0f2a08 commit 7d5fb2b

19 files changed

Lines changed: 869 additions & 128 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ config/tsup.frameworks.config.bundled*
1515
publish-summary.json
1616
**/.svelte-kit
1717
**/*/*.tgz
18+
.envrc
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Recursively walks a response object and collects Transferable entries
3+
* (ArrayBuffer from typed arrays, raw ArrayBuffer, ImageBitmap).
4+
* Uses a Set to deduplicate — postMessage throws on duplicate transferables.
5+
*
6+
* Depth-limited to 6 to cover the deepest nesting in this codebase:
7+
* response.data.result[annotId][mode].data.data (AnnotationAppearanceMap)
8+
*/
9+
export function extractTransferables(obj: unknown, depth = 6): Transferable[] {
10+
const set = new Set<Transferable>();
11+
collect(obj, depth, set);
12+
return Array.from(set);
13+
}
14+
15+
function collect(value: unknown, depth: number, set: Set<Transferable>): void {
16+
if (depth < 0 || value == null) return;
17+
18+
if (value instanceof ArrayBuffer) {
19+
set.add(value);
20+
return;
21+
}
22+
23+
if (ArrayBuffer.isView(value)) {
24+
set.add(value.buffer as ArrayBuffer);
25+
return;
26+
}
27+
28+
if (typeof value !== 'object') return;
29+
30+
if (Array.isArray(value)) {
31+
for (let i = 0; i < value.length; i++) {
32+
collect(value[i], depth - 1, set);
33+
}
34+
return;
35+
}
36+
37+
for (const key in value as Record<string, unknown>) {
38+
collect((value as Record<string, unknown>)[key], depth - 1, set);
39+
}
40+
}

packages/engines/src/lib/orchestrator/pdf-engine.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
322322
execute: () => this.executor.renderPageRaw(doc, page, options),
323323
meta: { docId: doc.id, pageIndex: page.index, operation: 'renderPageRaw' },
324324
},
325-
{ priority: Priority.HIGH },
325+
{ priority: options?.priority ?? Priority.HIGH },
326326
);
327327
}
328328

@@ -337,7 +337,7 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
337337
execute: () => this.executor.renderPageRect(doc, page, rect, options),
338338
meta: { docId: doc.id, pageIndex: page.index, operation: 'renderPageRectRaw' },
339339
},
340-
{ priority: Priority.HIGH },
340+
{ priority: options?.priority ?? Priority.HIGH },
341341
);
342342
}
343343

@@ -488,9 +488,14 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
488488
height: rawImageData.height,
489489
};
490490

491+
const sizeLabel = `${rawImageData.width}x${rawImageData.height}`;
492+
this.logger.perf(LOG_SOURCE, 'encodeImage', 'encode', 'Begin', sizeLabel);
491493
this.options
492494
.imageConverter(() => plainImageData, imageType, quality)
493-
.then((result) => resultTask.resolve(result))
495+
.then((result) => {
496+
this.logger.perf(LOG_SOURCE, 'encodeImage', 'encode', 'End', sizeLabel);
497+
resultTask.resolve(result);
498+
})
494499
.catch((error) => resultTask.reject({ code: PdfErrorCode.Unknown, message: String(error) }));
495500
}
496501

packages/engines/src/lib/orchestrator/pdfium-native-runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Logger, NoopLogger, Task, TaskError, PdfErrorCode } from '@embedpdf/models';
22
import { PdfiumNative } from '../pdfium/engine';
33
import { init } from '@embedpdf/pdfium';
4+
import { extractTransferables } from '../extract-transferables';
45

56
const LOG_SOURCE = 'PdfiumNativeRunner';
67
const LOG_CATEGORY = 'Worker';
@@ -255,7 +256,7 @@ export class PdfiumNativeRunner {
255256
*/
256257
private respond(response: WorkerResponse): void {
257258
this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'Sending response:', response.type);
258-
self.postMessage(response);
259+
self.postMessage(response, { transfer: extractTransferables(response) });
259260
}
260261

261262
/**

packages/engines/src/lib/webworker/runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
PdfErrorCode,
1010
TaskReturn,
1111
} from '@embedpdf/models';
12+
import { extractTransferables } from '../extract-transferables';
1213

1314
/**
1415
* Request body that represent method calls of PdfEngine, it contains the
@@ -472,6 +473,6 @@ export class EngineRunner {
472473
*/
473474
respond(response: Response) {
474475
this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'runner respond: ', response);
475-
self.postMessage(response);
476+
self.postMessage(response, { transfer: extractTransferables(response) });
476477
}
477478
}

packages/models/src/pdf.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2940,6 +2940,10 @@ export interface PdfRenderOptions {
29402940
* Image quality (0-1) for jpeg and png
29412941
*/
29422942
imageQuality?: number;
2943+
/**
2944+
* Scheduling priority (higher = more urgent). Only used by the orchestrator engine.
2945+
*/
2946+
priority?: number;
29432947
}
29442948

29452949
export interface ConvertToBlobOptions {

packages/models/src/task.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,61 @@ export class Task<R, D, P = unknown> {
271271
}
272272
}
273273

274+
/**
275+
* Transform the result of this task using a mapping function.
276+
* Returns a new Task that resolves with the mapped value.
277+
* Aborting the derived task also aborts the source task.
278+
*
279+
* @param fn - mapping function (may return a value or a Promise)
280+
* @returns a new Task that resolves with the mapped result
281+
*/
282+
map<U>(fn: (result: R) => U | Promise<U>): Task<U, D> {
283+
const derived = new Task<U, D>();
284+
285+
// Wire abort propagation: aborting derived aborts source
286+
const originalAbort = derived.abort.bind(derived);
287+
derived.abort = (reason: D) => {
288+
this.abort(reason);
289+
originalAbort(reason);
290+
};
291+
292+
this.wait(
293+
(result) => {
294+
if (derived.state.stage !== TaskStage.Pending) return;
295+
try {
296+
const mapped = fn(result);
297+
if (mapped instanceof Promise) {
298+
mapped.then(
299+
(value) => {
300+
if (derived.state.stage === TaskStage.Pending) {
301+
derived.resolve(value);
302+
}
303+
},
304+
(err) => {
305+
if (derived.state.stage === TaskStage.Pending) {
306+
derived.reject(err);
307+
}
308+
},
309+
);
310+
} else {
311+
derived.resolve(mapped);
312+
}
313+
} catch (err) {
314+
if (derived.state.stage === TaskStage.Pending) {
315+
derived.reject(err as D);
316+
}
317+
}
318+
},
319+
(error) => {
320+
if (derived.state.stage === TaskStage.Pending) {
321+
derived.fail(error);
322+
}
323+
},
324+
);
325+
326+
return derived;
327+
}
328+
274329
/**
275330
* add a progress callback
276331
* @param cb - progress callback

packages/plugin-render/src/lib/render-plugin.ts

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BasePlugin, PluginRegistry } from '@embedpdf/core';
2+
import { PdfDocumentObject, PdfPageObject, PdfRenderPageOptions } from '@embedpdf/models';
23
import {
34
RenderCapability,
45
RenderPageOptions,
@@ -32,6 +33,9 @@ export class RenderPlugin extends BasePlugin<RenderPluginConfig, RenderCapabilit
3233
renderPageRect: (options: RenderPageRectOptions) => this.renderPageRect(options),
3334
renderPageRaw: (options: RenderPageOptions) => this.renderPageRaw(options),
3435
renderPageRectRaw: (options: RenderPageRectOptions) => this.renderPageRectRaw(options),
36+
renderPageBitmap: (options: RenderPageOptions) => this.renderPageBitmap(options),
37+
renderPageRectBitmap: (options: RenderPageRectOptions) => this.renderPageRectBitmap(options),
38+
renderMode: this.config.renderMode ?? 'blob',
3539

3640
// Document-scoped operations
3741
forDocument: (documentId: string) => this.createRenderScope(documentId),
@@ -49,14 +53,21 @@ export class RenderPlugin extends BasePlugin<RenderPluginConfig, RenderCapabilit
4953
renderPageRaw: (options: RenderPageOptions) => this.renderPageRaw(options, documentId),
5054
renderPageRectRaw: (options: RenderPageRectOptions) =>
5155
this.renderPageRectRaw(options, documentId),
56+
renderPageBitmap: (options: RenderPageOptions) => this.renderPageBitmap(options, documentId),
57+
renderPageRectBitmap: (options: RenderPageRectOptions) =>
58+
this.renderPageRectBitmap(options, documentId),
59+
renderMode: this.config.renderMode ?? 'blob',
5260
};
5361
}
5462

5563
// ─────────────────────────────────────────────────────────
56-
// Core Operations
64+
// Helpers
5765
// ─────────────────────────────────────────────────────────
5866

59-
private renderPage({ pageIndex, options }: RenderPageOptions, documentId?: string) {
67+
private resolveDocAndPage(
68+
pageIndex: number,
69+
documentId?: string,
70+
): { doc: PdfDocumentObject; page: PdfPageObject } {
6071
const id = documentId ?? this.getActiveDocumentId();
6172
const coreDoc = this.coreState.core.documents[id];
6273

@@ -69,90 +80,92 @@ export class RenderPlugin extends BasePlugin<RenderPluginConfig, RenderCapabilit
6980
throw new Error(`Page ${pageIndex} not found in document ${id}`);
7081
}
7182

72-
const mergedOptions = {
83+
return { doc: coreDoc.document, page };
84+
}
85+
86+
private mergeImageOptions(options?: PdfRenderPageOptions): PdfRenderPageOptions {
87+
return {
7388
...(options ?? {}),
7489
withForms: options?.withForms ?? this.config.withForms ?? false,
7590
withAnnotations: options?.withAnnotations ?? this.config.withAnnotations ?? false,
7691
imageType: options?.imageType ?? this.config.defaultImageType ?? 'image/png',
7792
imageQuality: options?.imageQuality ?? this.config.defaultImageQuality ?? 0.92,
7893
};
79-
80-
return this.engine.renderPage(coreDoc.document, page, mergedOptions);
8194
}
8295

83-
private renderPageRect({ pageIndex, rect, options }: RenderPageRectOptions, documentId?: string) {
84-
const id = documentId ?? this.getActiveDocumentId();
85-
const coreDoc = this.coreState.core.documents[id];
86-
87-
if (!coreDoc?.document) {
88-
throw new Error(`Document ${id} not loaded`);
89-
}
90-
91-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
92-
if (!page) {
93-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
94-
}
95-
96-
const mergedOptions = {
96+
private mergeRawOptions(options?: PdfRenderPageOptions): PdfRenderPageOptions {
97+
return {
9798
...(options ?? {}),
9899
withForms: options?.withForms ?? this.config.withForms ?? false,
99100
withAnnotations: options?.withAnnotations ?? this.config.withAnnotations ?? false,
100-
imageType: options?.imageType ?? this.config.defaultImageType ?? 'image/png',
101-
imageQuality: options?.imageQuality ?? this.config.defaultImageQuality ?? 0.92,
102101
};
103-
104-
return this.engine.renderPageRect(coreDoc.document, page, rect, mergedOptions);
105102
}
106103

107104
// ─────────────────────────────────────────────────────────
108-
// Raw Rendering (returns ImageDataLike, skips encoding)
105+
// Core Operations
109106
// ─────────────────────────────────────────────────────────
110107

111-
private renderPageRaw({ pageIndex, options }: RenderPageOptions, documentId?: string) {
112-
const id = documentId ?? this.getActiveDocumentId();
113-
const coreDoc = this.coreState.core.documents[id];
114-
115-
if (!coreDoc?.document) {
116-
throw new Error(`Document ${id} not loaded`);
117-
}
108+
private renderPage({ pageIndex, options }: RenderPageOptions, documentId?: string) {
109+
const { doc, page } = this.resolveDocAndPage(pageIndex, documentId);
110+
return this.engine.renderPage(doc, page, this.mergeImageOptions(options));
111+
}
118112

119-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
120-
if (!page) {
121-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
122-
}
113+
private renderPageRect({ pageIndex, rect, options }: RenderPageRectOptions, documentId?: string) {
114+
const { doc, page } = this.resolveDocAndPage(pageIndex, documentId);
115+
return this.engine.renderPageRect(doc, page, rect, this.mergeImageOptions(options));
116+
}
123117

124-
const mergedOptions = {
125-
...(options ?? {}),
126-
withForms: options?.withForms ?? this.config.withForms ?? false,
127-
withAnnotations: options?.withAnnotations ?? this.config.withAnnotations ?? false,
128-
};
118+
// ─────────────────────────────────────────────────────────
119+
// Raw Rendering (returns ImageDataLike, skips encoding)
120+
// ─────────────────────────────────────────────────────────
129121

130-
return this.engine.renderPageRaw(coreDoc.document, page, mergedOptions);
122+
private renderPageRaw({ pageIndex, options }: RenderPageOptions, documentId?: string) {
123+
const { doc, page } = this.resolveDocAndPage(pageIndex, documentId);
124+
return this.engine.renderPageRaw(doc, page, this.mergeRawOptions(options));
131125
}
132126

133127
private renderPageRectRaw(
134128
{ pageIndex, rect, options }: RenderPageRectOptions,
135129
documentId?: string,
136130
) {
137-
const id = documentId ?? this.getActiveDocumentId();
138-
const coreDoc = this.coreState.core.documents[id];
139-
140-
if (!coreDoc?.document) {
141-
throw new Error(`Document ${id} not loaded`);
142-
}
131+
const { doc, page } = this.resolveDocAndPage(pageIndex, documentId);
132+
return this.engine.renderPageRectRaw(doc, page, rect, this.mergeRawOptions(options));
133+
}
143134

144-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
145-
if (!page) {
146-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
147-
}
135+
// ─────────────────────────────────────────────────────────
136+
// Bitmap Rendering (raw → createImageBitmap on main thread)
137+
// ─────────────────────────────────────────────────────────
148138

149-
const mergedOptions = {
150-
...(options ?? {}),
151-
withForms: options?.withForms ?? this.config.withForms ?? false,
152-
withAnnotations: options?.withAnnotations ?? this.config.withAnnotations ?? false,
153-
};
139+
private renderPageBitmap({ pageIndex, options }: RenderPageOptions, documentId?: string) {
140+
const { doc, page } = this.resolveDocAndPage(pageIndex, documentId);
141+
return this.engine
142+
.renderPageRaw(doc, page, {
143+
...this.mergeRawOptions(options),
144+
priority: 3,
145+
})
146+
.map(async (raw) => {
147+
const sizeLabel = `${raw.width}x${raw.height}`;
148+
this.logger.perf('RenderPlugin', 'createImageBitmap', 'call', 'Begin', sizeLabel);
149+
const bmp = await createImageBitmap(new ImageData(raw.data, raw.width, raw.height));
150+
this.logger.perf('RenderPlugin', 'createImageBitmap', 'call', 'End', sizeLabel);
151+
return bmp;
152+
});
153+
}
154154

155-
return this.engine.renderPageRectRaw(coreDoc.document, page, rect, mergedOptions);
155+
private renderPageRectBitmap(
156+
{ pageIndex, rect, options }: RenderPageRectOptions,
157+
documentId?: string,
158+
) {
159+
const { doc, page } = this.resolveDocAndPage(pageIndex, documentId);
160+
return this.engine
161+
.renderPageRectRaw(doc, page, rect, this.mergeRawOptions(options))
162+
.map(async (raw) => {
163+
const sizeLabel = `${raw.width}x${raw.height}`;
164+
this.logger.perf('RenderPlugin', 'createImageBitmap', 'call', 'Begin', sizeLabel);
165+
const bmp = await createImageBitmap(new ImageData(raw.data, raw.width, raw.height));
166+
this.logger.perf('RenderPlugin', 'createImageBitmap', 'call', 'End', sizeLabel);
167+
return bmp;
168+
});
156169
}
157170

158171
// ─────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)