Skip to content

Commit 0921304

Browse files
committed
chore(bench): compare default and non-default shapes
1 parent e6c20f5 commit 0921304

9 files changed

Lines changed: 265 additions & 0 deletions

File tree

assembly/__benches__/large.bench.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { JSON } from "..";
22
import { expect } from "../__tests__/lib";
33
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
4+
import { nonDefaultValues } from "./lib/nondefault";
45

56

67
@json
@@ -169,9 +170,12 @@ class Repo {
169170
// Create instances and assign fields directly
170171
const v1 = new Repo();
171172
const v2 = `{"id":132935648,"node_id":"MDEwOlJlcG9zaXRvcnkxMzI5MzU2NDg=","name":"boysenberry-repo-1","full_name":"octocat/boysenberry-repo-1","private":true,"owner":{"login":"octocat","id":583231,"node_id":"MDQ6VXNlcjU4MzIzMQ==","avatar_url":"https://avatars.githubusercontent.com/u/583231?v=4","gravatar_id":"","url":"https://api.github.com/users/octocat","html_url":"https://github.com/octocat","followers_url":"https://api.github.com/users/octocat/followers","following_url":"https://api.github.com/users/octocat/following{/other_user}","gists_url":"https://api.github.com/users/octocat/gists{/gist_id}","starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/octocat/subscriptions","organizations_url":"https://api.github.com/users/octocat/orgs","repos_url":"https://api.github.com/users/octocat/repos","events_url":"https://api.github.com/users/octocat/events{/privacy}","received_events_url":"https://api.github.com/users/octocat/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/octocat/boysenberry-repo-1","description":"Testing","fork":true,"url":"https://api.github.com/repos/octocat/boysenberry-repo-1","forks_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/forks","keys_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/keys{/key_id}","collaborators_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/teams","hooks_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/hooks","issue_events_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/issues/events{/number}","events_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/events","assignees_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/assignees{/user}","branches_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/branches{/branch}","tags_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/tags","blobs_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/refs{/sha}","trees_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/trees{/sha}","statuses_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/statuses/{sha}","languages_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/languages","stargazers_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/stargazers","contributors_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/contributors","subscribers_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/subscribers","subscription_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/subscription","commits_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/commits{/sha}","git_commits_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/git/commits{/sha}","comments_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/comments{/number}","issue_comment_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/issues/comments{/number}","contents_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/contents/{+path}","compare_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/compare/{base}...{head}","merges_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/merges","archive_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/downloads","issues_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/issues{/number}","pulls_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/pulls{/number}","milestones_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/milestones{/number}","notifications_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/labels{/name}","releases_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/releases{/id}","deployments_url":"https://api.github.com/repos/octocat/boysenberry-repo-1/deployments","created_at":"2018-05-10T17:51:29Z","updated_at":"2025-05-24T02:01:19Z","pushed_at":"2024-05-26T07:02:05Z","git_url":"git://github.com/octocat/boysenberry-repo-1.git","ssh_url":"git@github.com:octocat/boysenberry-repo-1.git","clone_url":"https://github.com/octocat/boysenberry-repo-1.git","svn_url":"https://github.com/octocat/boysenberry-repo-1","homepage":"","size":4,"stargazers_count":332,"watchers_count":332,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":20,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":20,"open_issues":1,"watchers":332,"default_branch":"master"}`;
173+
const nonDefaultJson = nonDefaultValues(v2);
174+
const nonDefaultValue = JSON.parse<Repo>(nonDefaultJson);
172175
const byteLength: usize = utf8ByteLength(v2);
173176
expect(JSON.stringify(v1)).toBe(v2);
174177
expect(JSON.stringify(JSON.parse<Repo>(v2))).toBe(v2);
178+
expect(JSON.stringify(nonDefaultValue)).toBe(nonDefaultJson);
175179
bench(
176180
"Serialize Large API Response",
177181
() => {
@@ -191,6 +195,25 @@ bench(
191195
);
192196
dumpToFile("large", "deserialize");
193197

198+
bench(
199+
"Serialize Large API Response (non-default)",
200+
() => {
201+
blackbox(JSON.stringify(nonDefaultValue));
202+
},
203+
100_000,
204+
utf8ByteLength(nonDefaultJson),
205+
);
206+
dumpToFile("large-nondefault", "serialize");
207+
bench(
208+
"Deserialize Large API Response (non-default)",
209+
() => {
210+
blackbox(JSON.parse<Repo>(nonDefaultJson));
211+
},
212+
100_000,
213+
utf8ByteLength(nonDefaultJson),
214+
);
215+
dumpToFile("large-nondefault", "deserialize");
216+
194217
// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
195218
const objLarge = JSON.parse<JSON.Obj>(v2);
196219
bench(
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Produces a same-shape JSON fixture whose string, numeric, and boolean values
3+
* differ from the source while object keys and field order remain unchanged.
4+
* Nulls stay null because their replacement depends on the destination type.
5+
*/
6+
export function nonDefaultValues(src: string): string {
7+
let out = "";
8+
let segment = 0;
9+
let i = 0;
10+
11+
while (i < src.length) {
12+
const code = src.charCodeAt(i);
13+
if (code == 34) {
14+
const start = i++;
15+
let escaped = false;
16+
while (i < src.length) {
17+
const inner = src.charCodeAt(i);
18+
if (escaped) {
19+
escaped = false;
20+
} else if (inner == 92) {
21+
escaped = true;
22+
} else if (inner == 34) {
23+
break;
24+
}
25+
i++;
26+
}
27+
28+
let next = i + 1;
29+
while (next < src.length) {
30+
const after = src.charCodeAt(next);
31+
if (after != 32 && after != 9 && after != 10 && after != 13) break;
32+
next++;
33+
}
34+
// A quoted token followed by ':' is an object key. Prefix every other
35+
// quoted token so even empty/default strings miss literal specialization.
36+
if (next >= src.length || src.charCodeAt(next) != 58) {
37+
out += src.slice(segment, start + 1) + "!";
38+
segment = start + 1;
39+
}
40+
i++;
41+
continue;
42+
}
43+
44+
if (code >= 48 && code <= 57) {
45+
out +=
46+
src.slice(segment, i) +
47+
String.fromCharCode(48 + ((code - 48 + 1) % 10));
48+
segment = ++i;
49+
continue;
50+
}
51+
52+
if (code == 116 && src.startsWith("true", i)) {
53+
out += src.slice(segment, i) + "false";
54+
i += 4;
55+
segment = i;
56+
continue;
57+
}
58+
if (code == 102 && src.startsWith("false", i)) {
59+
out += src.slice(segment, i) + "true";
60+
i += 5;
61+
segment = i;
62+
continue;
63+
}
64+
i++;
65+
}
66+
67+
return out + src.slice(segment);
68+
}

assembly/__benches__/medium.bench.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { JSON } from "..";
22
import { expect } from "../__tests__/lib";
33
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
4+
import { nonDefaultValues } from "./lib/nondefault";
45

56

67
@json
@@ -58,6 +59,8 @@ class MediumAPIResponse {
5859

5960
const v1 = new MediumAPIResponse();
6061
const v2: string = JSON.stringify<MediumAPIResponse>(v1);
62+
const nonDefaultJson = nonDefaultValues(v2);
63+
const nonDefaultValue = JSON.parse<MediumAPIResponse>(nonDefaultJson);
6164
const byteLength: usize = utf8ByteLength(v2);
6265
const v2Ptr: usize = changetype<usize>(v2);
6366
const v2End: usize = v2Ptr + (v2.length << 1);
@@ -72,6 +75,7 @@ reusable.recent_activity[3] = new RecentActivity();
7275
reusable.recent_activity[4] = new RecentActivity();
7376
expect(JSON.stringify(v1)).toBe(v2);
7477
expect(JSON.stringify(JSON.parse<MediumAPIResponse>(v2))).toBe(v2);
78+
expect(JSON.stringify(nonDefaultValue)).toBe(nonDefaultJson);
7579
bench(
7680
"Serialize Medium API Response",
7781
() => {
@@ -91,6 +95,25 @@ bench(
9195
);
9296
dumpToFile("medium", "deserialize");
9397

98+
bench(
99+
"Serialize Medium API Response (non-default)",
100+
() => {
101+
blackbox(JSON.stringify<MediumAPIResponse>(nonDefaultValue));
102+
},
103+
500_000,
104+
utf8ByteLength(nonDefaultJson),
105+
);
106+
dumpToFile("medium-nondefault", "serialize");
107+
bench(
108+
"Deserialize Medium API Response (non-default)",
109+
() => {
110+
blackbox(JSON.parse<MediumAPIResponse>(nonDefaultJson));
111+
},
112+
500_000,
113+
utf8ByteLength(nonDefaultJson),
114+
);
115+
dumpToFile("medium-nondefault", "deserialize");
116+
94117
// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
95118
const objMedium = JSON.parse<JSON.Obj>(v2);
96119
bench(

assembly/__benches__/small.bench.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { JSON } from "..";
22
import { expect } from "../__tests__/lib";
33
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
4+
import { nonDefaultValues } from "./lib/nondefault";
45

56

67
@json
@@ -13,9 +14,12 @@ class SessionStatusResponse {
1314
}
1415
const v1 = new SessionStatusResponse();
1516
const v2: string = JSON.stringify<SessionStatusResponse>(v1);
17+
const nonDefaultJson = nonDefaultValues(v2);
18+
const nonDefaultValue = JSON.parse<SessionStatusResponse>(nonDefaultJson);
1619
const byteLength: usize = utf8ByteLength(v2);
1720
expect(JSON.stringify(v1)).toBe(v2);
1821
expect(JSON.stringify(JSON.parse<SessionStatusResponse>(v2))).toBe(v2);
22+
expect(JSON.stringify(nonDefaultValue)).toBe(nonDefaultJson);
1923
bench(
2024
"Serialize Small API Response",
2125
() => {
@@ -35,6 +39,25 @@ bench(
3539
);
3640
dumpToFile("small", "deserialize");
3741

42+
bench(
43+
"Serialize Small API Response (non-default)",
44+
() => {
45+
blackbox(JSON.stringify<SessionStatusResponse>(nonDefaultValue));
46+
},
47+
5_000_000,
48+
utf8ByteLength(nonDefaultJson),
49+
);
50+
dumpToFile("small-nondefault", "serialize");
51+
bench(
52+
"Deserialize Small API Response (non-default)",
53+
() => {
54+
blackbox(JSON.parse<SessionStatusResponse>(nonDefaultJson));
55+
},
56+
5_000_000,
57+
utf8ByteLength(nonDefaultJson),
58+
);
59+
dumpToFile("small-nondefault", "deserialize");
60+
3861
// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
3962
const objSmall = JSON.parse<JSON.Obj>(v2);
4063
bench(

assembly/__benches__/token.bench.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { JSON } from "../";
22
import { expect } from "../__tests__/lib";
33
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
4+
import { nonDefaultValues } from "./lib/nondefault";
45

56

67
@json
@@ -10,8 +11,11 @@ class Token {
1011
}
1112
const v1 = new Token();
1213
const v2 = JSON.stringify(v1);
14+
const nonDefaultJson = nonDefaultValues(v2);
15+
const nonDefaultValue = JSON.parse<Token>(nonDefaultJson);
1316
expect(JSON.stringify(v1)).toBe(v2);
1417
expect(JSON.stringify(JSON.parse<Token>(v2))).toBe(v2);
18+
expect(JSON.stringify(nonDefaultValue)).toBe(nonDefaultJson);
1519
bench(
1620
"Serialize Token Object",
1721
() => {
@@ -31,6 +35,25 @@ bench(
3135
);
3236
dumpToFile("token", "deserialize");
3337

38+
bench(
39+
"Serialize Token Object (non-default)",
40+
() => {
41+
blackbox<string>(JSON.stringify(nonDefaultValue));
42+
},
43+
10_000_000,
44+
utf8ByteLength(nonDefaultJson),
45+
);
46+
dumpToFile("token-nondefault", "serialize");
47+
bench(
48+
"Deserialize Token Object (non-default)",
49+
() => {
50+
blackbox<Token>(JSON.parse<Token>(nonDefaultJson));
51+
},
52+
10_000_000,
53+
utf8ByteLength(nonDefaultJson),
54+
);
55+
dumpToFile("token-nondefault", "deserialize");
56+
3457
// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
3558
const objToken = JSON.parse<JSON.Obj>(v2);
3659
bench(

assembly/__benches__/vec3.bench.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { JSON } from "..";
22
import { expect } from "../__tests__/lib";
33
import { bench, blackbox, dumpToFile, utf8ByteLength } from "./lib/bench";
4+
import { nonDefaultValues } from "./lib/nondefault";
45

56

67
@json
@@ -11,8 +12,11 @@ class Vec3 {
1112
}
1213
const v1: Vec3 = { x: 1, y: 2, z: 3 };
1314
const v2 = JSON.stringify(v1);
15+
const nonDefaultJson = nonDefaultValues(v2);
16+
const nonDefaultValue = JSON.parse<Vec3>(nonDefaultJson);
1417
expect(JSON.stringify(v1)).toBe(v2);
1518
expect(JSON.stringify(JSON.parse<Vec3>(v2))).toBe(v2);
19+
expect(JSON.stringify(nonDefaultValue)).toBe(nonDefaultJson);
1620
bench(
1721
"Serialize Vec3",
1822
() => {
@@ -32,6 +36,25 @@ bench(
3236
);
3337
dumpToFile("vec3", "deserialize");
3438

39+
bench(
40+
"Serialize Vec3 (non-default)",
41+
() => {
42+
blackbox(JSON.stringify(nonDefaultValue));
43+
},
44+
12_800_000,
45+
utf8ByteLength(nonDefaultJson),
46+
);
47+
dumpToFile("vec3-nondefault", "serialize");
48+
bench(
49+
"Deserialize Vec3 (non-default)",
50+
() => {
51+
blackbox(JSON.parse<Vec3>(nonDefaultJson));
52+
},
53+
12_800_000,
54+
utf8ByteLength(nonDefaultJson),
55+
);
56+
dumpToFile("vec3-nondefault", "deserialize");
57+
3558
// Dynamic JSON.Obj variant of the same payload (typed struct vs JSON.Obj).
3659
const objVec3 = JSON.parse<JSON.Obj>(v2);
3760
bench(

scripts/build-charts.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ mkdir -p ./build/charts
2828
# json-as vs JavaScript, small synthetic payloads (overview).
2929
JSON_CHART_RUNTIME="$CHART_RUNTIME" bun ./scripts/build-overview-serialize.ts
3030
JSON_CHART_RUNTIME="$CHART_RUNTIME" bun ./scripts/build-overview-deserialize.ts
31+
# Typed-struct fixtures with every non-null scalar/string value changed from defaults.
32+
# Render each chart in its own process: chart.js plugins retain module state
33+
# between canvases and can otherwise drop the second chart's legend text.
34+
JSON_CHART_RUNTIME="$CHART_RUNTIME" JSON_BENCH_KIND=serialize bun ./scripts/build-default-values.ts
35+
JSON_CHART_RUNTIME="$CHART_RUNTIME" JSON_BENCH_KIND=deserialize bun ./scripts/build-default-values.ts
3136
# String / object throughput vs payload size (full range + <=1MB zoom).
3237
JSON_CHART_RUNTIME="$CHART_RUNTIME" bun ./scripts/build-string-serialize.ts
3338
JSON_CHART_RUNTIME="$CHART_RUNTIME" bun ./scripts/build-string-deserialize.ts

scripts/build-default-values.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
createBarChart,
3+
generateChart,
4+
readASBenchResult,
5+
type BenchKind,
6+
type BenchResult,
7+
} from "./lib/bench-utils";
8+
import { BASE, MODE_BARS, rgba } from "./lib/palette";
9+
10+
const PAYLOADS: Record<string, string> = {
11+
vec3: "3D Vector\n(19b)",
12+
token: "Token\n(49b)",
13+
small: "Small Payload\n(108b)",
14+
medium: "Medium Payload\n(1.1kb)",
15+
large: "Large Payload\n(5.5kb)",
16+
};
17+
18+
const MODES = ["naive", "swar", "simd"] as const;
19+
const COLORS = [
20+
MODE_BARS[1],
21+
{ bg: rgba("orange", 0.35), border: BASE.orange },
22+
MODE_BARS[2],
23+
{ bg: rgba("jungleGreen", 0.35), border: BASE.jungleGreen },
24+
MODE_BARS[3],
25+
{ bg: rgba("pacificBlue", 0.35), border: BASE.pacificBlue },
26+
];
27+
28+
const requestedKind = process.env["JSON_BENCH_KIND"];
29+
const kinds: BenchKind[] =
30+
requestedKind == "serialize" || requestedKind == "deserialize"
31+
? [requestedKind]
32+
: ["serialize", "deserialize"];
33+
34+
for (const kind of kinds) {
35+
const chartData: Record<string, BenchResult[]> = {};
36+
for (const payload of Object.keys(PAYLOADS)) {
37+
chartData[payload] = MODES.flatMap((mode) => [
38+
readASBenchResult(payload, kind, mode),
39+
readASBenchResult(`${payload}-nondefault`, kind, mode),
40+
]);
41+
}
42+
43+
const operation = kind == "serialize" ? "Serialization" : "Deserialization";
44+
const out = `./build/charts/default-values-${kind}`;
45+
const dims = { width: 1200, height: 700 };
46+
// Chart.js mutates configuration objects while laying them out. Build a
47+
// fresh config for each format so the SVG render cannot affect the PNG.
48+
for (const extension of ["svg", "png"]) {
49+
const config = createBarChart(chartData, PAYLOADS, {
50+
title: `${operation}: default vs non-default values`,
51+
yLabel: "Throughput (MB/s)",
52+
xLabel: "",
53+
datasetLabels: [
54+
"NAIVE default",
55+
"NAIVE non-default",
56+
"SWAR default",
57+
"SWAR non-default",
58+
"SIMD default",
59+
"SIMD non-default",
60+
],
61+
colors: COLORS,
62+
labelAnchor: "end",
63+
labelFontSize: 11,
64+
labelRotation: -90,
65+
});
66+
generateChart(config, `${out}.${extension}`, dims);
67+
}
68+
}

0 commit comments

Comments
 (0)