serialbench 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/scripts/push-to-data.sh +65 -0
- data/.github/workflows/benchmark.yml +106 -160
- data/.github/workflows/release.yml +68 -13
- data/Gemfile +1 -1
- data/config/benchmarks/full-json.yml +25 -0
- data/config/benchmarks/full-toml.yml +25 -0
- data/config/benchmarks/full-xml.yml +26 -0
- data/config/benchmarks/full-yaml.yml +25 -0
- data/config/benchmarks/full.yml +9 -4
- data/lib/serialbench/benchmark_runner.rb +20 -23
- data/lib/serialbench/cli/benchmark_cli.rb +1 -1
- data/lib/serialbench/cli.rb +1 -8
- data/lib/serialbench/models/benchmark_result.rb +2 -0
- data/lib/serialbench/serializers/base_serializer.rb +14 -2
- data/lib/serialbench/serializers/xml/base_xml_serializer.rb +6 -10
- data/lib/serialbench/serializers/xml/leptris_serializer.rb +15 -8
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +2 -7
- data/lib/serialbench/serializers/xml/oga_serializer.rb +4 -0
- data/lib/serialbench/version.rb +1 -1
- data/lib/serialbench.rb +1 -1
- metadata +8 -38
- data/lib/serialbench/templates/assets/css/benchmark_report.css +0 -535
- data/lib/serialbench/templates/assets/css/format_based.css +0 -474
- data/lib/serialbench/templates/assets/css/themes.css +0 -589
- data/lib/serialbench/templates/assets/js/chart_helpers.js +0 -411
- data/lib/serialbench/templates/assets/js/dashboard.js +0 -795
- data/lib/serialbench/templates/assets/js/navigation.js +0 -142
- data/lib/serialbench/templates/base.liquid +0 -49
- data/lib/serialbench/templates/format_based.liquid +0 -507
- data/lib/serialbench/templates/partials/chart_section.liquid +0 -4
- data/site/.gitignore +0 -5
- data/site/README.md +0 -32
- data/site/astro.config.mjs +0 -12
- data/site/package-lock.json +0 -6160
- data/site/package.json +0 -22
- data/site/public/favicon.svg +0 -4
- data/site/scripts/gen-sample-data.mjs +0 -154
- data/site/src/components/AvailabilityMatrix.astro +0 -61
- data/site/src/components/CalibratedLeaderboard.vue +0 -134
- data/site/src/components/CitationBox.vue +0 -50
- data/site/src/components/DashboardConsole.vue +0 -193
- data/site/src/components/FeaturesTable.astro +0 -97
- data/site/src/components/MemoryPanel.vue +0 -44
- data/site/src/components/OpsChart.vue +0 -135
- data/site/src/config.ts +0 -18
- data/site/src/layouts/Layout.astro +0 -85
- data/site/src/lib/channels.ts +0 -24
- data/site/src/lib/dashboard.ts +0 -269
- data/site/src/pages/index.astro +0 -44
- data/site/src/pages/library/[slug].astro +0 -128
- data/site/src/pages/methodology.astro +0 -55
- data/site/src/styles/global.css +0 -103
- data/site/tsconfig.json +0 -5
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { Payload } from '../lib/dashboard';
|
|
3
|
-
import { channelColor } from '../lib/channels';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
payload: Payload;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { payload } = Astro.props;
|
|
10
|
-
|
|
11
|
-
const FEATURE_LABELS: Record<string, string> = {
|
|
12
|
-
streaming: 'Streaming (SAX)',
|
|
13
|
-
stax: 'StAX (pull)',
|
|
14
|
-
xpath: 'XPath',
|
|
15
|
-
namespaces: 'Namespaces',
|
|
16
|
-
validation: 'Validation',
|
|
17
|
-
pretty_print: 'Pretty print',
|
|
18
|
-
symbol_keys: 'Symbol keys',
|
|
19
|
-
custom_types: 'Custom types',
|
|
20
|
-
comments: 'Comments',
|
|
21
|
-
arrays_of_tables: 'Arrays of tables',
|
|
22
|
-
inline_tables: 'Inline tables',
|
|
23
|
-
multiline_strings: 'Multiline strings',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const FEATURE_ORDER = Object.keys(FEATURE_LABELS);
|
|
27
|
-
|
|
28
|
-
const libraries = payload.libraries ?? [];
|
|
29
|
-
const formats = [...new Set(libraries.map((l) => l.format))].sort();
|
|
30
|
-
|
|
31
|
-
function columnsFor(group: typeof libraries): string[] {
|
|
32
|
-
const keys = new Set<string>();
|
|
33
|
-
for (const lib of group) for (const key of Object.keys(lib.features)) keys.add(key);
|
|
34
|
-
return FEATURE_ORDER.filter((k) => keys.has(k));
|
|
35
|
-
}
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
<section aria-label="Feature comparison" class="mt-12">
|
|
39
|
-
<div class="mb-3">
|
|
40
|
-
<p class="font-mono text-[10px] uppercase tracking-[0.14em] text-inkmute">Feature comparison — what each API offers, beyond speed</p>
|
|
41
|
-
<p class="font-mono text-[11px] text-inkmute mt-1">
|
|
42
|
-
Capabilities declared by the adapters · all libraries benchmark DOM-style parsing; a StAX column appears when a binding exposes a pull API.
|
|
43
|
-
</p>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
formats.map((format) => {
|
|
48
|
-
const group = libraries.filter((l) => l.format === format);
|
|
49
|
-
const cols = columnsFor(group);
|
|
50
|
-
return (
|
|
51
|
-
<div class="mb-6">
|
|
52
|
-
<h3 class="font-mono text-[11px] uppercase tracking-[0.14em] text-inkdim mb-2">{format}</h3>
|
|
53
|
-
<div class="overflow-x-auto border border-line bg-panel">
|
|
54
|
-
<table class="w-full border-collapse">
|
|
55
|
-
<thead>
|
|
56
|
-
<tr>
|
|
57
|
-
<th class="text-left font-mono text-[10px] uppercase tracking-[0.14em] text-inkmute font-normal px-3 py-2 border-b border-line">Library</th>
|
|
58
|
-
<th class="text-left font-mono text-[10px] uppercase tracking-[0.14em] text-inkmute font-normal px-3 py-2 border-b border-line">Version</th>
|
|
59
|
-
{cols.map((c) => (
|
|
60
|
-
<th class="text-center font-mono text-[10px] uppercase tracking-wider text-inkmute font-normal px-3 py-2 border-b border-line whitespace-nowrap">
|
|
61
|
-
{FEATURE_LABELS[c] ?? c}
|
|
62
|
-
</th>
|
|
63
|
-
))}
|
|
64
|
-
</tr>
|
|
65
|
-
</thead>
|
|
66
|
-
<tbody>
|
|
67
|
-
{group.map((lib) => (
|
|
68
|
-
<tr>
|
|
69
|
-
<td class="px-3 py-1.5 border-b border-line/50 font-mono text-[12px] whitespace-nowrap">
|
|
70
|
-
<a
|
|
71
|
-
class="flex items-center gap-2 hover:text-phosphor transition-colors"
|
|
72
|
-
href={`${import.meta.env.BASE_URL}library/${lib.name}/`}
|
|
73
|
-
>
|
|
74
|
-
<span class="h-2 w-2 rounded-full" style={`background: ${channelColor(lib.name)}`} />
|
|
75
|
-
{lib.name}
|
|
76
|
-
</a>
|
|
77
|
-
</td>
|
|
78
|
-
<td class="px-3 py-1.5 border-b border-line/50 font-mono text-[12px] text-inkmute">{lib.version}</td>
|
|
79
|
-
{cols.map((c) => (
|
|
80
|
-
<td class="px-3 py-1.5 border-b border-line/50 text-center">
|
|
81
|
-
{lib.features[c] ? (
|
|
82
|
-
<span class="inline-block h-2 w-2 rounded-full" style={`background: ${channelColor(lib.name)}`} title={FEATURE_LABELS[c] ?? c} />
|
|
83
|
-
) : (
|
|
84
|
-
<span class="inline-block h-2 w-2 rounded-full border border-line" title="not supported" />
|
|
85
|
-
)}
|
|
86
|
-
</td>
|
|
87
|
-
))}
|
|
88
|
-
</tr>
|
|
89
|
-
))}
|
|
90
|
-
</tbody>
|
|
91
|
-
</table>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
</section>
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { MemoryRow } from '../lib/dashboard';
|
|
3
|
-
import { formatMemory } from '../lib/dashboard';
|
|
4
|
-
import { channelColor } from '../lib/channels';
|
|
5
|
-
|
|
6
|
-
const props = defineProps<{ rows: MemoryRow[]; size: string }>();
|
|
7
|
-
|
|
8
|
-
const maxAllocated = () => Math.max(...props.rows.map((r) => r.allocated), 0.0001);
|
|
9
|
-
</script>
|
|
10
|
-
|
|
11
|
-
<template>
|
|
12
|
-
<figure class="border border-line bg-panel p-4">
|
|
13
|
-
<figcaption class="mb-4">
|
|
14
|
-
<p class="font-mono text-[10px] uppercase tracking-[0.14em] text-inkmute">Memory — {{ size }} input, lower is better</p>
|
|
15
|
-
<p class="font-mono text-[11px] text-inkmute mt-1">
|
|
16
|
-
Solid: allocated during operation · outline: retained afterwards
|
|
17
|
-
</p>
|
|
18
|
-
</figcaption>
|
|
19
|
-
|
|
20
|
-
<ul class="space-y-2.5" role="list">
|
|
21
|
-
<li v-for="row in rows" :key="row.serializer" class="grid grid-cols-[minmax(6rem,8rem)_1fr_minmax(6rem,7rem)] gap-x-3 items-center">
|
|
22
|
-
<span class="flex items-center gap-2 font-mono text-[13px] truncate min-w-0" :style="{ color: channelColor(row.serializer) }">
|
|
23
|
-
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ background: channelColor(row.serializer) }" />
|
|
24
|
-
{{ row.serializer }}
|
|
25
|
-
</span>
|
|
26
|
-
|
|
27
|
-
<span class="relative h-5 graticule rounded-sm">
|
|
28
|
-
<span
|
|
29
|
-
class="absolute inset-y-[2px] left-0 rounded-[2px]"
|
|
30
|
-
:style="{ width: `${(row.allocated / maxAllocated()) * 100}%`, background: channelColor(row.serializer), opacity: 0.4 }"
|
|
31
|
-
/>
|
|
32
|
-
<span
|
|
33
|
-
class="absolute inset-y-[2px] left-0 rounded-[2px] border"
|
|
34
|
-
:style="{ width: `${(row.retained / maxAllocated()) * 100}%`, borderColor: channelColor(row.serializer) }"
|
|
35
|
-
/>
|
|
36
|
-
</span>
|
|
37
|
-
|
|
38
|
-
<span class="text-right font-mono text-[12px] tabular text-inkdim">
|
|
39
|
-
{{ formatMemory(row.allocated) }}
|
|
40
|
-
</span>
|
|
41
|
-
</li>
|
|
42
|
-
</ul>
|
|
43
|
-
</figure>
|
|
44
|
-
</template>
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
3
|
-
import {
|
|
4
|
-
BarController,
|
|
5
|
-
BarElement,
|
|
6
|
-
CategoryScale,
|
|
7
|
-
Chart,
|
|
8
|
-
Legend,
|
|
9
|
-
LinearScale,
|
|
10
|
-
LogarithmicScale,
|
|
11
|
-
Tooltip,
|
|
12
|
-
} from 'chart.js';
|
|
13
|
-
import type { Payload } from '../lib/dashboard';
|
|
14
|
-
|
|
15
|
-
Chart.register(BarController, BarElement, CategoryScale, LinearScale, LogarithmicScale, Tooltip, Legend);
|
|
16
|
-
|
|
17
|
-
const props = defineProps<{
|
|
18
|
-
payload: Payload;
|
|
19
|
-
op: string;
|
|
20
|
-
format: string;
|
|
21
|
-
sizes: string[];
|
|
22
|
-
envKey: string;
|
|
23
|
-
serializers: string[];
|
|
24
|
-
}>();
|
|
25
|
-
|
|
26
|
-
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
27
|
-
let chart: Chart | null = null;
|
|
28
|
-
|
|
29
|
-
function cssVar(name: string): string {
|
|
30
|
-
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function ipsAt(serializer: string, size: string): number {
|
|
34
|
-
const metric = props.payload.combined_results[props.op]?.[size]?.[props.format]?.[serializer]?.[props.envKey] as
|
|
35
|
-
| { iterations_per_second: number }
|
|
36
|
-
| undefined;
|
|
37
|
-
return metric?.iterations_per_second ?? 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function orderedSerializers(): string[] {
|
|
41
|
-
const anchor = props.sizes[props.sizes.length - 1] ?? 'small';
|
|
42
|
-
return [...props.serializers].sort((a, b) => ipsAt(b, anchor) - ipsAt(a, anchor));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function build() {
|
|
46
|
-
if (!canvas.value) return;
|
|
47
|
-
chart?.destroy();
|
|
48
|
-
|
|
49
|
-
const grid = cssVar('--line');
|
|
50
|
-
const inkMute = cssVar('--ink-mute');
|
|
51
|
-
const phosphor = cssVar('--phosphor');
|
|
52
|
-
const mono = "'IBM Plex Mono', monospace";
|
|
53
|
-
|
|
54
|
-
const sizeShades: Record<string, number> = { small: 0.35, medium: 0.6, large: 1 };
|
|
55
|
-
|
|
56
|
-
chart = new Chart(canvas.value, {
|
|
57
|
-
type: 'bar',
|
|
58
|
-
data: {
|
|
59
|
-
labels: orderedSerializers(),
|
|
60
|
-
datasets: props.sizes.map((size) => ({
|
|
61
|
-
label: size,
|
|
62
|
-
data: orderedSerializers().map((s) => ipsAt(s, size)),
|
|
63
|
-
backgroundColor: props.sizes.map((s) => hexAlpha(phosphor, sizeShades[s] ?? 0.6)),
|
|
64
|
-
})),
|
|
65
|
-
},
|
|
66
|
-
options: {
|
|
67
|
-
indexAxis: 'y' as const,
|
|
68
|
-
responsive: true,
|
|
69
|
-
maintainAspectRatio: false,
|
|
70
|
-
animation: { duration: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 400 },
|
|
71
|
-
scales: {
|
|
72
|
-
x: {
|
|
73
|
-
type: 'logarithmic',
|
|
74
|
-
grid: { color: grid },
|
|
75
|
-
border: { color: grid },
|
|
76
|
-
ticks: { color: inkMute, font: { family: mono, size: 10 } },
|
|
77
|
-
},
|
|
78
|
-
y: {
|
|
79
|
-
grid: { display: false },
|
|
80
|
-
border: { color: grid },
|
|
81
|
-
ticks: { color: inkMute, font: { family: mono, size: 11 } },
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
plugins: {
|
|
85
|
-
legend: {
|
|
86
|
-
labels: { color: inkMute, font: { family: mono, size: 10 }, boxWidth: 10, boxHeight: 10 },
|
|
87
|
-
},
|
|
88
|
-
tooltip: {
|
|
89
|
-
callbacks: {
|
|
90
|
-
label: (ctx) => {
|
|
91
|
-
const ips = ctx.parsed.x as number;
|
|
92
|
-
return `${ctx.dataset.label}: ${ips < 10 ? ips.toFixed(2) : ips.toFixed(0)} ips (${(1000 / ips).toFixed(1)} ms/op)`;
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function hexAlpha(hex: string, alpha: number): string {
|
|
102
|
-
const clean = hex.replace('#', '');
|
|
103
|
-
const r = parseInt(clean.slice(0, 2), 16);
|
|
104
|
-
const g = parseInt(clean.slice(2, 4), 16);
|
|
105
|
-
const b = parseInt(clean.slice(4, 6), 16);
|
|
106
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let themeObserver: MutationObserver | null = null;
|
|
110
|
-
|
|
111
|
-
onMounted(() => {
|
|
112
|
-
build();
|
|
113
|
-
themeObserver = new MutationObserver(build);
|
|
114
|
-
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
onBeforeUnmount(() => {
|
|
118
|
-
themeObserver?.disconnect();
|
|
119
|
-
chart?.destroy();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
watch(() => [props.op, props.format, props.envKey, props.serializers.join(','), props.sizes.join(',')], build);
|
|
123
|
-
</script>
|
|
124
|
-
|
|
125
|
-
<template>
|
|
126
|
-
<figure class="border border-line bg-panel p-4">
|
|
127
|
-
<figcaption class="mb-3">
|
|
128
|
-
<p class="font-mono text-[10px] uppercase tracking-[0.14em] text-inkmute">Consistency across input sizes — log scale</p>
|
|
129
|
-
<p class="font-mono text-[11px] text-inkmute mt-1">A library that wins at every size is a safe default; one that wins only at {{ sizes[sizes.length - 1] }} fits that workload.</p>
|
|
130
|
-
</figcaption>
|
|
131
|
-
<div class="h-72">
|
|
132
|
-
<canvas ref="canvas" role="img" aria-label="Bar chart comparing serializers across input sizes"></canvas>
|
|
133
|
-
</div>
|
|
134
|
-
</figure>
|
|
135
|
-
</template>
|
data/site/src/config.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Site identity and navigation — the single place to change branding,
|
|
2
|
-
// base path usage, or links. Base path itself lives in astro.config.mjs
|
|
3
|
-
// (`site` + `base`); every internal link must be built from
|
|
4
|
-
// import.meta.env.BASE_URL, never a literal "/".
|
|
5
|
-
export const BASE = import.meta.env.BASE_URL;
|
|
6
|
-
|
|
7
|
-
export const SITE = {
|
|
8
|
-
name: 'Serialbench',
|
|
9
|
-
tagline: 'ruby serialization · measurement console',
|
|
10
|
-
repoUrl: 'https://github.com/metanorma/serialbench',
|
|
11
|
-
footer: 'serialbench · automated weekly runs · metanorma',
|
|
12
|
-
disclaimer: 'microbenchmarks indicate, they do not decide',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const NAV = [
|
|
16
|
-
{ label: 'Dashboard', href: BASE },
|
|
17
|
-
{ label: 'Methodology', href: `${BASE}methodology/` },
|
|
18
|
-
];
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import '@fontsource/ibm-plex-sans/400.css';
|
|
3
|
-
import '@fontsource/ibm-plex-sans/500.css';
|
|
4
|
-
import '@fontsource/ibm-plex-sans/600.css';
|
|
5
|
-
import '@fontsource/ibm-plex-mono/400.css';
|
|
6
|
-
import '@fontsource/ibm-plex-mono/500.css';
|
|
7
|
-
import '../styles/global.css';
|
|
8
|
-
import { SITE, NAV, BASE } from '../config';
|
|
9
|
-
|
|
10
|
-
interface Props {
|
|
11
|
-
title: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const { title, description = 'Cross-platform benchmarks of Ruby serialization libraries — XML, JSON, YAML, TOML.' } = Astro.props;
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
<!doctype html>
|
|
19
|
-
<html lang="en">
|
|
20
|
-
<head>
|
|
21
|
-
<meta charset="utf-8" />
|
|
22
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23
|
-
<title>{title}</title>
|
|
24
|
-
<meta name="description" content={description} />
|
|
25
|
-
<link rel="icon" type="image/svg+xml" href={`${BASE}favicon.svg`} />
|
|
26
|
-
<script is:inline>
|
|
27
|
-
const stored = localStorage.getItem('serialbench-theme');
|
|
28
|
-
const theme = stored ?? 'dark';
|
|
29
|
-
document.documentElement.dataset.theme = theme;
|
|
30
|
-
</script>
|
|
31
|
-
</head>
|
|
32
|
-
<body class="min-h-screen flex flex-col">
|
|
33
|
-
<header class="border-b border-line">
|
|
34
|
-
<div class="mx-auto max-w-6xl px-4 sm:px-6 min-h-14 py-2 flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
|
|
35
|
-
<a href={BASE} class="flex items-center gap-3 group">
|
|
36
|
-
<span class="font-mono text-[11px] uppercase tracking-[0.14em] border border-line px-2 py-1 text-inkdim group-hover:border-phosphor group-hover:text-phosphor transition-colors">
|
|
37
|
-
{SITE.name}
|
|
38
|
-
</span>
|
|
39
|
-
<span class="hidden md:inline font-mono text-[11px] uppercase tracking-[0.14em] text-inkmute">
|
|
40
|
-
{SITE.tagline}
|
|
41
|
-
</span>
|
|
42
|
-
</a>
|
|
43
|
-
<nav class="flex items-center gap-0.5 sm:gap-1 font-mono text-[11px] uppercase tracking-[0.14em]">
|
|
44
|
-
<a href={NAV[0].href} class="px-2 py-1 text-inkdim hover:text-phosphor transition-colors">{NAV[0].label}</a>
|
|
45
|
-
<a href={NAV[1].href} class="px-2 py-1 text-inkdim hover:text-phosphor transition-colors">{NAV[1].label}</a>
|
|
46
|
-
<button
|
|
47
|
-
id="theme-toggle"
|
|
48
|
-
type="button"
|
|
49
|
-
class="px-2 py-1 text-inkdim hover:text-phosphor transition-colors"
|
|
50
|
-
aria-label="Toggle light and dark theme"
|
|
51
|
-
>
|
|
52
|
-
<span data-theme-label>Light</span>
|
|
53
|
-
</button>
|
|
54
|
-
</nav>
|
|
55
|
-
</div>
|
|
56
|
-
</header>
|
|
57
|
-
|
|
58
|
-
<main class="flex-1 mx-auto w-full max-w-6xl px-4 sm:px-6 py-8">
|
|
59
|
-
<slot />
|
|
60
|
-
</main>
|
|
61
|
-
|
|
62
|
-
<footer class="border-t border-line">
|
|
63
|
-
<div class="mx-auto max-w-6xl px-4 sm:px-6 py-6 flex flex-wrap items-center justify-between gap-3 font-mono text-[11px] uppercase tracking-[0.14em] text-inkmute">
|
|
64
|
-
<span>{SITE.footer}</span>
|
|
65
|
-
<span>{SITE.disclaimer}</span>
|
|
66
|
-
</div>
|
|
67
|
-
</footer>
|
|
68
|
-
|
|
69
|
-
<script is:inline>
|
|
70
|
-
const button = document.getElementById('theme-toggle');
|
|
71
|
-
const label = button?.querySelector('[data-theme-label]');
|
|
72
|
-
const sync = () => {
|
|
73
|
-
if (!label) return;
|
|
74
|
-
label.textContent = document.documentElement.dataset.theme === 'light' ? 'Dark' : 'Light';
|
|
75
|
-
};
|
|
76
|
-
sync();
|
|
77
|
-
button?.addEventListener('click', () => {
|
|
78
|
-
const next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
|
|
79
|
-
document.documentElement.dataset.theme = next;
|
|
80
|
-
localStorage.setItem('serialbench-theme', next);
|
|
81
|
-
sync();
|
|
82
|
-
});
|
|
83
|
-
</script>
|
|
84
|
-
</body>
|
|
85
|
-
</html>
|
data/site/src/lib/channels.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Channel assignment: every serializer keeps one color everywhere on the site
|
|
2
|
-
// (bars, chart strokes, availability dots), like scope channels on a bench
|
|
3
|
-
// instrument. Single source of truth — components import from here.
|
|
4
|
-
export const CHANNELS: Record<string, string> = {
|
|
5
|
-
leptris: '#5fd4e8',
|
|
6
|
-
nokogiri: '#f5a623',
|
|
7
|
-
ox: '#e85d9e',
|
|
8
|
-
oga: '#9b8cff',
|
|
9
|
-
rexml: '#77879e',
|
|
10
|
-
'libxml-ruby': '#d97b4a',
|
|
11
|
-
oj: '#6ee7a0',
|
|
12
|
-
json: '#b8c4d0',
|
|
13
|
-
rapidjson: '#7be0d3',
|
|
14
|
-
yajl: '#e8c56a',
|
|
15
|
-
psych: '#c9a0dc',
|
|
16
|
-
syck: '#8c7ae0',
|
|
17
|
-
'toml-rb': '#e88b6a',
|
|
18
|
-
tomlib: '#6ab0e8',
|
|
19
|
-
tomlrb: '#d4e87b',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function channelColor(serializer: string): string {
|
|
23
|
-
return CHANNELS[serializer] ?? '#8fa3bf';
|
|
24
|
-
}
|
data/site/src/lib/dashboard.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
// Typed access to the dashboard payload produced by the Ruby CLI
|
|
2
|
-
// (`serialbench resultset export-data` — same shape the old Liquid site
|
|
3
|
-
// consumed): combined_results[op][size][format][serializer][envKey].
|
|
4
|
-
|
|
5
|
-
export interface PerfMetric {
|
|
6
|
-
iterations_per_second: number;
|
|
7
|
-
time_per_iteration: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface MemoryMetric {
|
|
11
|
-
allocated_memory: number;
|
|
12
|
-
retained_memory: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Environment {
|
|
16
|
-
ruby_version: string;
|
|
17
|
-
ruby_platform?: string;
|
|
18
|
-
os: string;
|
|
19
|
-
arch: string;
|
|
20
|
-
timestamp: string;
|
|
21
|
-
source_file?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Payload {
|
|
25
|
-
combined_results: Record<string, Record<string, Record<string, Record<string, Record<string, PerfMetric | MemoryMetric>>>>>;
|
|
26
|
-
environments: Record<string, Environment>;
|
|
27
|
-
metadata: {
|
|
28
|
-
resultset_name?: string;
|
|
29
|
-
resultset_description?: string;
|
|
30
|
-
total_runs?: number;
|
|
31
|
-
generated_at: string;
|
|
32
|
-
};
|
|
33
|
-
libraries?: LibraryInfo[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const OPERATIONS = ['parsing', 'generation', 'streaming'] as const;
|
|
37
|
-
export type Operation = (typeof OPERATIONS)[number];
|
|
38
|
-
|
|
39
|
-
export const OPERATION_LABELS: Record<Operation, string> = {
|
|
40
|
-
parsing: 'Parse',
|
|
41
|
-
generation: 'Generate',
|
|
42
|
-
streaming: 'Stream',
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export type Size = 'small' | 'medium' | 'large';
|
|
46
|
-
|
|
47
|
-
export interface LeaderRow {
|
|
48
|
-
serializer: string;
|
|
49
|
-
ips: number;
|
|
50
|
-
msPerIteration: number;
|
|
51
|
-
ratioToRef: number | null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function metricsFor(payload: Payload, op: string, size: string, format: string): Record<string, Record<string, PerfMetric>> {
|
|
55
|
-
const byFormat = payload.combined_results[op]?.[size]?.[format] ?? {};
|
|
56
|
-
return byFormat as Record<string, Record<string, PerfMetric>>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const FORMAT_ORDER = ['xml', 'json', 'yaml', 'toml'];
|
|
60
|
-
|
|
61
|
-
export function availableFormats(payload: Payload): string[] {
|
|
62
|
-
const formats = new Set<string>();
|
|
63
|
-
for (const sizes of Object.values(payload.combined_results.parsing ?? {})) {
|
|
64
|
-
for (const format of Object.keys(sizes)) formats.add(format);
|
|
65
|
-
}
|
|
66
|
-
return [...formats].sort((a, b) => {
|
|
67
|
-
const ia = FORMAT_ORDER.indexOf(a);
|
|
68
|
-
const ib = FORMAT_ORDER.indexOf(b);
|
|
69
|
-
return (ia === -1 ? FORMAT_ORDER.length : ia) - (ib === -1 ? FORMAT_ORDER.length : ib);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function availableOperations(payload: Payload, format: string): Operation[] {
|
|
74
|
-
return OPERATIONS.filter((op) => {
|
|
75
|
-
const sizes = payload.combined_results[op];
|
|
76
|
-
if (!sizes) return false;
|
|
77
|
-
return Object.values(sizes).some((byFormat) => format in byFormat);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function availableSizes(payload: Payload, op: string, format: string): string[] {
|
|
82
|
-
const sizes = payload.combined_results[op];
|
|
83
|
-
if (!sizes) return [];
|
|
84
|
-
return Object.keys(sizes).filter((size) => {
|
|
85
|
-
const byFormat = sizes[size];
|
|
86
|
-
return byFormat != null && format in byFormat && Object.keys(byFormat[format]).length > 0;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function environmentList(payload: Payload): { key: string; env: Environment }[] {
|
|
91
|
-
return Object.entries(payload.environments)
|
|
92
|
-
.map(([key, env]) => ({ key, env }))
|
|
93
|
-
.sort((a, b) => b.env.timestamp.localeCompare(a.env.timestamp));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Env keys are runner-shaped ("macos-26-intel-ruby-3.4") when produced by CI,
|
|
97
|
-
// or "macos-arm64-ruby-3.4" (os-arch) for docker/local/legacy results.
|
|
98
|
-
export function environmentLabel(key: string, env: Environment): string {
|
|
99
|
-
const arch = env.arch === 'x86_64' ? 'x64' : env.arch;
|
|
100
|
-
const runner = key.replace(/-ruby-[^-]*$/, '');
|
|
101
|
-
const m = runner.match(/^(ubuntu|macos|windows)-(\d+(?:\.\d+)?)(?:-(arm|intel|large))?$/);
|
|
102
|
-
if (m) {
|
|
103
|
-
const osName = m[1].replace('ubuntu', 'Ubuntu').replace('windows', 'Windows').replace('macos', 'macOS');
|
|
104
|
-
const variant = m[3] === 'intel' ? ' Intel' : '';
|
|
105
|
-
return `${osName} ${m[2]}${variant} · ${arch} · Ruby ${env.ruby_version}`;
|
|
106
|
-
}
|
|
107
|
-
const osName = env.os.replace('ubuntu', 'Ubuntu').replace('windows', 'Windows').replace('macos', 'macOS');
|
|
108
|
-
return `${osName} · ${arch} · Ruby ${env.ruby_version}`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Short runner label for table headers: "macos-26-intel-ruby-3.4" → "macOS 26 Intel"
|
|
112
|
-
export function runnerShortLabel(key: string, env: Environment): string {
|
|
113
|
-
const runner = key.replace(/-ruby-[^-]*$/, '');
|
|
114
|
-
const m = runner.match(/^(ubuntu|macos|windows)-(\d+(?:\.\d+)?)(?:-(arm|intel|large))?$/);
|
|
115
|
-
if (m) {
|
|
116
|
-
const osName = m[1].replace('ubuntu', 'Ubuntu').replace('windows', 'Windows').replace('macos', 'macOS');
|
|
117
|
-
return `${osName} ${m[2]}${m[3] === 'intel' ? ' Intel' : ''}`;
|
|
118
|
-
}
|
|
119
|
-
return runner || env.os;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function serializersFor(payload: Payload, format: string): string[] {
|
|
123
|
-
const serializers = new Set<string>();
|
|
124
|
-
for (const sizes of Object.values(payload.combined_results.parsing ?? {})) {
|
|
125
|
-
if (format in sizes) Object.keys(sizes[format]).forEach((s) => serializers.add(s));
|
|
126
|
-
}
|
|
127
|
-
for (const sizes of Object.values(payload.combined_results.generation ?? {})) {
|
|
128
|
-
if (format in sizes) Object.keys(sizes[format]).forEach((s) => serializers.add(s));
|
|
129
|
-
}
|
|
130
|
-
return [...serializers].sort();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Ranking at one operation/size/env; ratios computed against `reference`. */
|
|
134
|
-
export function leaderboard(payload: Payload, op: string, size: string, format: string, envKey: string, reference: string | null): LeaderRow[] {
|
|
135
|
-
const bySerializer = metricsFor(payload, op, size, format);
|
|
136
|
-
const rows: LeaderRow[] = [];
|
|
137
|
-
for (const [serializer, byEnv] of Object.entries(bySerializer)) {
|
|
138
|
-
const metric = byEnv[envKey];
|
|
139
|
-
if (!metric || metric.iterations_per_second == null) continue;
|
|
140
|
-
rows.push({
|
|
141
|
-
serializer,
|
|
142
|
-
ips: metric.iterations_per_second,
|
|
143
|
-
msPerIteration: metric.time_per_iteration * 1000,
|
|
144
|
-
ratioToRef: null,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
rows.sort((a, b) => b.ips - a.ips);
|
|
148
|
-
const ref = rows.find((r) => r.serializer === reference) ?? rows[0];
|
|
149
|
-
if (ref) {
|
|
150
|
-
for (const row of rows) row.ratioToRef = row.ips > 0 ? ref.ips / row.ips : null;
|
|
151
|
-
}
|
|
152
|
-
return rows;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface MemoryRow {
|
|
156
|
-
serializer: string;
|
|
157
|
-
allocated: number;
|
|
158
|
-
retained: number;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function memoryRows(payload: Payload, size: string, format: string, envKey: string): MemoryRow[] {
|
|
162
|
-
const bySerializer = metricsFor(payload, 'memory', size, format);
|
|
163
|
-
const rows: MemoryRow[] = [];
|
|
164
|
-
for (const [serializer, byEnv] of Object.entries(bySerializer)) {
|
|
165
|
-
const metric = byEnv[envKey] as MemoryMetric | undefined;
|
|
166
|
-
if (!metric || metric.allocated_memory == null) continue;
|
|
167
|
-
rows.push({ serializer, allocated: metric.allocated_memory, retained: metric.retained_memory });
|
|
168
|
-
}
|
|
169
|
-
return rows.sort((a, b) => a.allocated - b.allocated);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** serializer × env coverage — "will it run on my stack", derived from the data itself. */
|
|
173
|
-
export function availability(payload: Payload): { serializers: string[]; envKeys: string[]; cells: Map<string, Set<string>> } {
|
|
174
|
-
const serializers = new Set<string>();
|
|
175
|
-
const cells = new Map<string, Set<string>>();
|
|
176
|
-
for (const [op, sizes] of Object.entries(payload.combined_results)) {
|
|
177
|
-
if (op === 'memory') continue;
|
|
178
|
-
for (const byFormat of Object.values(sizes)) {
|
|
179
|
-
for (const bySerializer of Object.values(byFormat)) {
|
|
180
|
-
for (const [serializer, byEnv] of Object.entries(bySerializer)) {
|
|
181
|
-
serializers.add(serializer);
|
|
182
|
-
if (!cells.has(serializer)) cells.set(serializer, new Set());
|
|
183
|
-
for (const envKey of Object.keys(byEnv)) cells.get(serializer)!.add(envKey);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const envKeys = Object.keys(payload.environments);
|
|
189
|
-
return { serializers: [...serializers].sort(), envKeys, cells };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function formatIps(ips: number): string {
|
|
193
|
-
if (ips >= 1000) return `${(ips / 1000).toFixed(ips >= 10000 ? 0 : 1)}k`;
|
|
194
|
-
if (ips >= 100) return ips.toFixed(0);
|
|
195
|
-
if (ips >= 10) return ips.toFixed(1);
|
|
196
|
-
return ips.toFixed(2);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function formatMemory(mb: number): string {
|
|
200
|
-
if (mb >= 1000) return `${(mb / 1000).toFixed(2)} GB`;
|
|
201
|
-
if (mb >= 1) return `${mb.toFixed(1)} MB`;
|
|
202
|
-
return `${(mb * 1024).toFixed(0)} KB`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function primaryEnvKey(payload: Payload): string {
|
|
206
|
-
return environmentList(payload)[0]?.key ?? '';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export interface LibraryStat {
|
|
210
|
-
op: string;
|
|
211
|
-
size: string;
|
|
212
|
-
ips: number;
|
|
213
|
-
rank: number;
|
|
214
|
-
fieldSize: number;
|
|
215
|
-
bestSerializer: string;
|
|
216
|
-
ratioToBest: number | null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** All measured stats for one serializer on the primary environment. */
|
|
220
|
-
export function libraryStats(payload: Payload, serializer: string, envKey: string): LibraryStat[] {
|
|
221
|
-
const stats: LibraryStat[] = [];
|
|
222
|
-
for (const [op, sizes] of Object.entries(payload.combined_results)) {
|
|
223
|
-
if (op === 'memory') continue;
|
|
224
|
-
for (const [size, formats] of Object.entries(sizes)) {
|
|
225
|
-
for (const bySerializer of Object.values(formats)) {
|
|
226
|
-
if (!(serializer in bySerializer)) continue;
|
|
227
|
-
const metric = bySerializer[serializer][envKey] as PerfMetric | undefined;
|
|
228
|
-
if (!metric || metric.iterations_per_second == null) continue;
|
|
229
|
-
const best = Object.entries(bySerializer)
|
|
230
|
-
.map(([name, byEnv]) => ({ name, ips: (byEnv[envKey] as PerfMetric | undefined)?.iterations_per_second ?? 0 }))
|
|
231
|
-
.sort((a, b) => b.ips - a.ips);
|
|
232
|
-
const ips = metric.iterations_per_second;
|
|
233
|
-
stats.push({
|
|
234
|
-
op,
|
|
235
|
-
size,
|
|
236
|
-
ips,
|
|
237
|
-
rank: best.findIndex((b) => b.name === serializer) + 1,
|
|
238
|
-
fieldSize: best.length,
|
|
239
|
-
bestSerializer: best[0].name,
|
|
240
|
-
ratioToBest: best[0].ips > 0 ? best[0].ips / ips : null,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return stats;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Formats a serializer participated in. */
|
|
249
|
-
export function formatsForSerializer(payload: Payload, serializer: string): string[] {
|
|
250
|
-
const formats = new Set<string>();
|
|
251
|
-
for (const sizes of Object.values(payload.combined_results.parsing ?? {})) {
|
|
252
|
-
for (const [format, bySerializer] of Object.entries(sizes)) {
|
|
253
|
-
if (serializer in bySerializer) formats.add(format);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
for (const sizes of Object.values(payload.combined_results.generation ?? {})) {
|
|
257
|
-
for (const [format, bySerializer] of Object.entries(sizes)) {
|
|
258
|
-
if (serializer in bySerializer) formats.add(format);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return [...formats].sort();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export interface LibraryInfo {
|
|
265
|
-
name: string;
|
|
266
|
-
format: string;
|
|
267
|
-
version: string;
|
|
268
|
-
features: Record<string, boolean>;
|
|
269
|
-
}
|