Skip to content

Commit 467db63

Browse files
committed
perf(serdes): dedupe qrl chunk names
1 parent 4189020 commit 467db63

File tree

6 files changed

+130
-75
lines changed

6 files changed

+130
-75
lines changed

packages/qwik/src/core/shared/serdes/allocate.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { DeserializeContainer } from '../types';
1818
import { _UNINITIALIZED } from '../utils/constants';
1919
import { _constants, TypeIds, type Constants } from './constants';
2020
import { needsInflation } from './deser-proxy';
21-
import { parseQRL } from './qrl-to-string';
21+
import { createQRLWithBackChannel } from './qrl-to-string';
2222

2323
export const resolvers = new WeakMap<Promise<any>, [Function, Function]>();
2424
export const pendingStoreTargets = new Map<object, { t: TypeIds; v: unknown }>();
@@ -48,13 +48,17 @@ export const allocate = (container: DeserializeContainer, typeId: number, value:
4848
case TypeIds.Object:
4949
return {};
5050
case TypeIds.QRL:
51-
case TypeIds.PreloadQRL:
52-
const qrl =
53-
typeof value === 'number'
54-
? // root reference
55-
container.$getObjectById$(value)
56-
: value;
57-
return parseQRL(qrl as string);
51+
case TypeIds.PreloadQRL: {
52+
if (typeof value === 'string') {
53+
const data = value.split(' ').map(Number);
54+
const chunk = container.$getObjectById$(data[0]) as string;
55+
const symbol = container.$getObjectById$(data[1]) as string;
56+
const captureIds = data.length > 2 ? data.slice(2) : null;
57+
return createQRLWithBackChannel(chunk, symbol, captureIds);
58+
} else {
59+
return createQRLWithBackChannel('', String(value));
60+
}
61+
}
5862
case TypeIds.Task:
5963
return new Task(-1, -1, null!, null!, null!, null);
6064
case TypeIds.Resource: {

packages/qwik/src/core/shared/serdes/qrl-to-string.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,17 @@ import { assertDefined } from '../error/assert';
99
export function qrlToString(
1010
serializationContext: SerializationContext,
1111
value: QRLInternal | SyncQRLInternal
12-
) {
12+
): string;
13+
export function qrlToString(
14+
serializationContext: SerializationContext,
15+
value: QRLInternal | SyncQRLInternal,
16+
raw: true
17+
): [string, string, string[] | null];
18+
export function qrlToString(
19+
serializationContext: SerializationContext,
20+
value: QRLInternal | SyncQRLInternal,
21+
raw?: true
22+
): string | [string, string, string[] | null] {
1323
let symbol = value.$symbol$;
1424
let chunk = value.$chunk$;
1525

@@ -52,24 +62,35 @@ export function qrlToString(
5262
symbol = String(serializationContext.$addSyncFn$(null, 0, fn));
5363
}
5464

65+
if (!value.$capture$ && Array.isArray(value.$captureRef$) && value.$captureRef$.length > 0) {
66+
// We refer by id so every capture needs to be a root
67+
value.$capture$ = value.$captureRef$.map((ref) => `${serializationContext.$addRoot$(ref)}`);
68+
}
69+
if (raw) {
70+
return [chunk, symbol, value.$capture$];
71+
}
5572
let qrlStringInline = `${chunk}#${symbol}`;
56-
if (Array.isArray(value.$captureRef$) && value.$captureRef$.length > 0) {
57-
let serializedReferences = '';
58-
// hot-path optimization
59-
for (let i = 0; i < value.$captureRef$.length; i++) {
60-
if (i > 0) {
61-
serializedReferences += ' ';
62-
}
63-
// We refer by id so every capture needs to be a root
64-
serializedReferences += serializationContext.$addRoot$(value.$captureRef$[i]);
65-
}
66-
qrlStringInline += `[${serializedReferences}]`;
67-
} else if (value.$capture$ && value.$capture$.length > 0) {
73+
if (value.$capture$ && value.$capture$.length > 0) {
6874
qrlStringInline += `[${value.$capture$.join(' ')}]`;
6975
}
7076
return qrlStringInline;
71-
} /** Parses "chunk#hash[...rootRef]" */
77+
}
78+
79+
export function createQRLWithBackChannel(
80+
chunk: string,
81+
symbol: string,
82+
captureIds?: number[] | null
83+
): QRLInternal<any> {
84+
let qrlRef = null;
85+
if (isDev && chunk === QRL_RUNTIME_CHUNK) {
86+
const backChannel: Map<string, Function> = (globalThis as any).__qrl_back_channel__;
87+
assertDefined(backChannel, 'Missing QRL_RUNTIME_CHUNK');
88+
qrlRef = backChannel.get(symbol);
89+
}
90+
return createQRL(chunk, symbol, qrlRef, null, captureIds!, null);
91+
}
7292

93+
/** Parses "chunk#hash[...rootRef]" */
7394
export function parseQRL(qrl: string): QRLInternal<any> {
7495
const hashIdx = qrl.indexOf('#');
7596
const captureStart = qrl.indexOf('[', hashIdx);
@@ -85,12 +106,7 @@ export function parseQRL(qrl: string): QRLInternal<any> {
85106
.filter((v) => v.length)
86107
.map((s) => parseInt(s, 10))
87108
: null;
88-
let qrlRef = null;
89-
if (isDev && chunk === QRL_RUNTIME_CHUNK) {
90-
const backChannel: Map<string, Function> = (globalThis as any).__qrl_back_channel__;
91-
assertDefined(backChannel, 'Missing QRL_RUNTIME_CHUNK');
92-
qrlRef = backChannel.get(symbol);
93-
}
94-
return createQRL(chunk, symbol, qrlRef, null, captureIds, null);
109+
return createQRLWithBackChannel(chunk, symbol, captureIds);
95110
}
111+
96112
export const QRL_RUNTIME_CHUNK = 'mock-chunk';

packages/qwik/src/core/shared/serdes/serdes.public.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSerializationContext } from '.';
1+
import { createSerializationContext } from './index';
22
import { assertTrue } from '../error/assert';
33
import type { DeserializeContainer } from '../types';
44
import { isNode, isElement } from '../utils/element';

packages/qwik/src/core/shared/serdes/serdes.unit.ts

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,12 @@ describe('shared-serialization', () => {
323323
expect(await dump(inlinedQrl(() => myVar + other, 'dump_qrl', [myVar, other])))
324324
.toMatchInlineSnapshot(`
325325
"
326-
0 QRL "mock-chunk#dump_qrl[1 2]"
326+
0 QRL "3 4 1 2"
327327
1 {number} 123
328328
2 {string} "hello"
329-
(47 chars)"
329+
3 {string} "mock-chunk"
330+
4 {string} "dump_qrl"
331+
(58 chars)"
330332
`);
331333
});
332334
it(title(TypeIds.Task), async () => {
@@ -344,7 +346,7 @@ describe('shared-serialization', () => {
344346
).toMatchInlineSnapshot(`
345347
"
346348
0 Task [
347-
QRL "mock-chunk#task_qrl[1]"
349+
QRL "2 3 1"
348350
{number} 0
349351
{number} 0
350352
RootRef 1
@@ -355,11 +357,13 @@ describe('shared-serialization', () => {
355357
]
356358
]
357359
1 Object [
358-
RootRef 2
360+
RootRef 4
359361
{number} 1
360362
]
361-
2 RootRef "0 5 0"
362-
(91 chars)"
363+
2 {string} "mock-chunk"
364+
3 {string} "task_qrl"
365+
4 RootRef "0 5 0"
366+
(102 chars)"
363367
`);
364368
});
365369
it(title(TypeIds.Resource), async () => {
@@ -388,9 +392,11 @@ describe('shared-serialization', () => {
388392
`
389393
"
390394
0 Component [
391-
QRL "mock-chunk#dump_component"
395+
QRL "1 2"
392396
]
393-
(37 chars)"
397+
1 {string} "mock-chunk"
398+
2 {string} "dump_component"
399+
(49 chars)"
394400
`
395401
);
396402
});
@@ -484,14 +490,19 @@ describe('shared-serialization', () => {
484490
Constant null
485491
{number} 2
486492
]
487-
4 PreloadQRL "mock-chunk#dirty[8]"
488-
5 PreloadQRL "mock-chunk#clean[8]"
489-
6 PreloadQRL "mock-chunk#never[8]"
490-
7 PreloadQRL "mock-chunk#always[8]"
493+
4 PreloadQRL "9 10 8"
494+
5 PreloadQRL "9 11 8"
495+
6 PreloadQRL "9 12 8"
496+
7 PreloadQRL "9 13 8"
491497
8 Signal [
492498
{number} 1
493499
]
494-
(171 chars)"
500+
9 {string} "mock-chunk"
501+
10 {string} "dirty"
502+
11 {string} "clean"
503+
12 {string} "never"
504+
13 {string} "always"
505+
(174 chars)"
495506
`);
496507
});
497508
it(title(TypeIds.SerializerSignal), async () => {
@@ -514,16 +525,18 @@ describe('shared-serialization', () => {
514525
expect(_dumpState(objs)).toMatchInlineSnapshot(`
515526
"
516527
0 ForwardRef 0
517-
1 PreloadQRL "mock-chunk#custom_createSerializer_qrl"
528+
1 PreloadQRL "3 4"
518529
2 SerializerSignal [
519530
RootRef 1
520531
Constant null
521532
{number} 4
522533
]
523-
3 ForwardRefs [
534+
3 {string} "mock-chunk"
535+
4 {string} "custom_createSerializer_qrl"
536+
5 ForwardRefs [
524537
2
525538
]
526-
(73 chars)"
539+
(85 chars)"
527540
`);
528541
});
529542
it(title(TypeIds.AsyncComputedSignal), async () => {
@@ -608,14 +621,19 @@ describe('shared-serialization', () => {
608621
Constant null
609622
{number} 2
610623
]
611-
4 PreloadQRL "mock-chunk#dirty[8]"
612-
5 PreloadQRL "mock-chunk#clean[8]"
613-
6 PreloadQRL "mock-chunk#never[8]"
614-
7 PreloadQRL "mock-chunk#always[8]"
624+
4 PreloadQRL "9 10 8"
625+
5 PreloadQRL "9 11 8"
626+
6 PreloadQRL "9 12 8"
627+
7 PreloadQRL "9 13 8"
615628
8 Signal [
616629
{number} 1
617630
]
618-
(231 chars)"
631+
9 {string} "mock-chunk"
632+
10 {string} "dirty"
633+
11 {string} "clean"
634+
12 {string} "never"
635+
13 {string} "always"
636+
(234 chars)"
619637
`);
620638
});
621639
it(title(TypeIds.Store), async () => {
@@ -950,12 +968,14 @@ describe('shared-serialization', () => {
950968
const objs = await serialize(qrl1, [qrl2]);
951969
expect(_dumpState(objs)).toMatchInlineSnapshot(`
952970
"
953-
0 QRL "mock-chunk#dump_qrl[2]"
971+
0 QRL "3 4 2"
954972
1 Array [
955973
RootRef 0
956974
]
957975
2 Object []
958-
(42 chars)"
976+
3 {string} "mock-chunk"
977+
4 {string} "dump_qrl"
978+
(53 chars)"
959979
`);
960980
});
961981
it('should dedupe identical root qrls', async () => {
@@ -967,10 +987,12 @@ describe('shared-serialization', () => {
967987
const objs = await serialize(qrl1, qrl2);
968988
expect(_dumpState(objs)).toMatchInlineSnapshot(`
969989
"
970-
0 QRL "mock-chunk#dump_qrl[2]"
990+
0 QRL "3 4 2"
971991
1 RootRef 0
972992
2 Object []
973-
(38 chars)"
993+
3 {string} "mock-chunk"
994+
4 {string} "dump_qrl"
995+
(49 chars)"
974996
`);
975997
});
976998
});
@@ -1014,15 +1036,17 @@ describe('shared-serialization', () => {
10141036
{string} "child"
10151037
ForwardRef 0
10161038
]
1017-
1 QRL "mock-chunk#dump_qrl[2]"
1039+
1 QRL "3 4 2"
10181040
2 Object [
10191041
{string} "should"
10201042
{string} "serialize"
10211043
]
1022-
3 ForwardRefs [
1044+
3 {string} "mock-chunk"
1045+
4 {string} "dump_qrl"
1046+
5 ForwardRefs [
10231047
2
10241048
]
1025-
(83 chars)"
1049+
(94 chars)"
10261050
`);
10271051
});
10281052
it('should serialize object after qrl', async () => {
@@ -1037,7 +1061,7 @@ describe('shared-serialization', () => {
10371061
const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent.child]);
10381062
expect(await dump(qrl, parent)).toMatchInlineSnapshot(`
10391063
"
1040-
0 QRL "mock-chunk#dump_qrl[2]"
1064+
0 QRL "3 4 2"
10411065
1 Object [
10421066
{string} "child"
10431067
ForwardRef 0
@@ -1046,10 +1070,12 @@ describe('shared-serialization', () => {
10461070
{string} "should"
10471071
{string} "serialize"
10481072
]
1049-
3 ForwardRefs [
1073+
3 {string} "mock-chunk"
1074+
4 {string} "dump_qrl"
1075+
5 ForwardRefs [
10501076
2
10511077
]
1052-
(83 chars)"
1078+
(94 chars)"
10531079
`);
10541080
});
10551081
});
@@ -1158,9 +1184,11 @@ describe('shared-serialization', () => {
11581184
{number} 1
11591185
]
11601186
]
1161-
1 QRL "mock-chunk#foo[2]"
1187+
1 QRL "3 4 2"
11621188
2 RootRef "0 0"
1163-
(55 chars)"
1189+
3 {string} "mock-chunk"
1190+
4 {string} "foo"
1191+
(66 chars)"
11641192
`);
11651193
// make sure shared1 is only serialized once
11661194
expect([objs[4], objs[5]]).toEqual([TypeIds.RootRef, '0 0']);

packages/qwik/src/core/shared/serdes/serialization-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { VNodeData } from '../../../server/vnode-data';
22
import type { _EFFECT_BACK_REF } from '../../internal';
33
import type { EffectProperty, EffectSubscription } from '../../reactive-primitives/types';
4-
import type { ISsrNode, SymbolToChunkResolver, StreamWriter } from '../../ssr/ssr-types';
4+
import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../../ssr/ssr-types';
55
import type { ResourceReturnInternal } from '../../use/use-resource';
66
import { qError, QError } from '../error/error';
77
import type { QRL } from '../qrl/qrl.public';

packages/qwik/src/core/shared/serdes/serialize.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,20 +164,27 @@ export async function serialize(serializationContext: SerializationContext): Pro
164164
output(TypeIds.Constant, Constants.Fragment);
165165
} else if (isQrl(value)) {
166166
if (!outputAsRootRef(value)) {
167-
const qrl = qrlToString(serializationContext, value);
168-
169-
// Since we map QRLs to strings, we need to keep track of this secondary mapping
170-
const existing = qrlMap.get(qrl);
171-
if (existing) {
172-
// We encountered the same QRL again, make it a root
173-
const ref = $addRoot$(existing);
174-
output(TypeIds.RootRef, ref);
175-
return;
167+
const [chunk, symbol, captureIds] = qrlToString(serializationContext, value, true);
168+
let data: string | number;
169+
if (chunk !== '') {
170+
// not a sync QRL, replace all parts with string references
171+
data = `${$addRoot$(chunk)} ${$addRoot$(symbol)}${captureIds ? ' ' + captureIds.join(' ') : ''}`;
172+
// Since we map QRLs to strings, we need to keep track of this secondary mapping
173+
const existing = qrlMap.get(data);
174+
if (existing) {
175+
// We encountered the same QRL again, make it a root
176+
const ref = $addRoot$(existing);
177+
output(TypeIds.RootRef, ref);
178+
return;
179+
} else {
180+
qrlMap.set(data, value);
181+
}
176182
} else {
177-
qrlMap.set(qrl, value);
183+
data = Number(symbol);
178184
}
185+
179186
const type = preloadQrls.has(value) ? TypeIds.PreloadQRL : TypeIds.QRL;
180-
output(type, qrl);
187+
output(type, data);
181188
}
182189
} else if (isQwikComponent(value)) {
183190
const [qrl]: [QRLInternal] = (value as any)[SERIALIZABLE_STATE];

0 commit comments

Comments
 (0)