Skip to content

Commit 1130aa8

Browse files
committed
Add an unofficial shared-memory ring buffer workload
Producer claims a slot with Atomics.add, writes the record with Atomics.store, and publishes it with Atomics.compareExchange. The consumer observes the slot with Atomics.load. Both sides run on one agent so the score does not depend on worker scheduling. The workload is not in the default suite. Run it with --test=atomics-ring-buffer.
1 parent c603c04 commit 1130aa8

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

‎JetStreamDriver.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,6 +2244,14 @@ let BENCHMARKS = [
22442244
worstCaseCount: 2,
22452245
tags: ["default", "js", "json", "inspector", "SeaMonster"],
22462246
}),
2247+
// Atomics
2248+
new DefaultBenchmark({
2249+
name: "atomics-ring-buffer",
2250+
files: [
2251+
"./atomics/benchmark.js",
2252+
],
2253+
tags: ["js", "atomics"],
2254+
}),
22472255
// BigInt
22482256
new AsyncBenchmark({
22492257
name: "bigint-noble-bls12-381",

‎atomics/benchmark.js‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright (C) 2026 Sergey Rubanov. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions
6+
* are met:
7+
* 1. Redistributions of source code must retain the above copyright
8+
* notice, this list of conditions and the following disclaimer.
9+
* 2. Redistributions in binary form must reproduce the above copyright
10+
* notice, this list of conditions and the following disclaimer in the
11+
* documentation and/or other materials provided with the distribution.
12+
*
13+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
14+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
15+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
16+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
17+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
19+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
*/
25+
26+
"use strict";
27+
28+
// Shared-memory ring buffer, the producer/consumer queue behind audio graphs
29+
// and worker-backed UI state. The producer claims a slot, writes a fixed-size
30+
// record, and publishes a sequence number the consumer observes. Both sides
31+
// run on this agent, so worker scheduling stays out of the score.
32+
// https://issues.chromium.org/issues/497018236
33+
34+
const CAPACITY = 1024;
35+
const MASK = CAPACITY - 1;
36+
const SLOT_WORDS = 4;
37+
const HEADER_WORDS = 2;
38+
const MESSAGES = 32768;
39+
const BATCH = 64;
40+
41+
const WRITE_CURSOR = 0;
42+
const READ_CURSOR = 1;
43+
44+
function slotBase(seq) {
45+
return HEADER_WORDS + (seq & MASK) * SLOT_WORDS;
46+
}
47+
48+
function payloadA(seq) {
49+
return (Math.imul(seq, 3) + 1) >>> 0;
50+
}
51+
52+
function payloadB(seq) {
53+
return (Math.imul(seq, 7) + 2) >>> 0;
54+
}
55+
56+
function payloadSum(seq, a, b) {
57+
return (a + b + seq) >>> 0;
58+
}
59+
60+
function expectedChecksum(count) {
61+
let checksum = 0;
62+
for (let seq = 0; seq < count; seq++) {
63+
const sum = payloadSum(seq, payloadA(seq), payloadB(seq));
64+
checksum = (checksum + sum) >>> 0;
65+
}
66+
return checksum;
67+
}
68+
69+
class RingBuffer {
70+
constructor() {
71+
const words = HEADER_WORDS + CAPACITY * SLOT_WORDS;
72+
this.view = new Uint32Array(new SharedArrayBuffer(words * 4));
73+
this.expected = expectedChecksum(MESSAGES);
74+
this.checksum = 0;
75+
this.reset();
76+
}
77+
78+
// Called from prepareForNextIteration, outside the timed region.
79+
reset() {
80+
this.view[WRITE_CURSOR] = 0;
81+
this.view[READ_CURSOR] = 0;
82+
for (let slot = 0; slot < CAPACITY; slot++)
83+
this.view[HEADER_WORDS + slot * SLOT_WORDS] = slot;
84+
this.checksum = 0;
85+
}
86+
87+
publish(seq) {
88+
const base = slotBase(seq);
89+
const sequence = Atomics.load(this.view, base);
90+
if (sequence !== seq) throw new Error(`sequence ${sequence} != ${seq}`);
91+
const a = payloadA(seq);
92+
const b = payloadB(seq);
93+
Atomics.store(this.view, base + 1, a);
94+
Atomics.store(this.view, base + 2, b);
95+
Atomics.store(this.view, base + 3, payloadSum(seq, a, b));
96+
const previous = Atomics.compareExchange(this.view, base, seq, (seq + 1) >>> 0);
97+
if (previous !== seq) throw new Error(`publish lost sequence ${seq}`);
98+
}
99+
100+
consume(seq) {
101+
const base = slotBase(seq);
102+
const readCursor = Atomics.load(this.view, READ_CURSOR);
103+
if (readCursor !== seq) throw new Error(`read cursor ${readCursor} != ${seq}`);
104+
const sequence = Atomics.load(this.view, base);
105+
if (sequence !== (seq + 1) >>> 0) throw new Error(`sequence ${sequence} != ${seq + 1}`);
106+
const a = Atomics.load(this.view, base + 1);
107+
const b = Atomics.load(this.view, base + 2);
108+
const sum = Atomics.load(this.view, base + 3);
109+
if (a !== payloadA(seq) || b !== payloadB(seq) || sum !== payloadSum(seq, a, b))
110+
throw new Error(`corrupt slot ${seq}`);
111+
Atomics.store(this.view, base, (seq + CAPACITY) >>> 0);
112+
Atomics.store(this.view, READ_CURSOR, (seq + 1) >>> 0);
113+
this.checksum = (this.checksum + sum) >>> 0;
114+
}
115+
116+
run() {
117+
for (let start = 0; start < MESSAGES; start += BATCH) {
118+
for (let i = 0; i < BATCH; i++) {
119+
const seq = Atomics.add(this.view, WRITE_CURSOR, 1);
120+
this.publish(seq);
121+
}
122+
for (let i = 0; i < BATCH; i++) this.consume(start + i);
123+
}
124+
if (this.checksum !== this.expected)
125+
throw new Error(`checksum ${this.checksum} != ${this.expected}`);
126+
const writeCursor = Atomics.load(this.view, WRITE_CURSOR);
127+
if (writeCursor !== MESSAGES) throw new Error(`write cursor ${writeCursor} != ${MESSAGES}`);
128+
}
129+
}
130+
131+
class Benchmark {
132+
constructor() {
133+
if (typeof SharedArrayBuffer !== "function" || typeof Atomics !== "object")
134+
throw new Error("atomics-ring-buffer requires SharedArrayBuffer and Atomics");
135+
this.ring = new RingBuffer();
136+
}
137+
138+
prepareForNextIteration() {
139+
this.ring.reset();
140+
}
141+
142+
runIteration() {
143+
this.ring.run();
144+
}
145+
}

‎in-depth.html‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,17 @@ <h3>
197197
existing files.
198198
Source code: <a href="generators/async-file-system.js">async-file-system.js</a>
199199
</dd>
200+
<dt id="atomics-ring-buffer">atomics-ring-buffer</dt>
201+
<dd>
202+
A shared-memory ring buffer, the pattern behind worker and main-thread queues
203+
such as audio graphs and UI state shared with a worker. The producer claims a
204+
slot with <code>Atomics.add</code>, writes the record with
205+
<code>Atomics.store</code>, and publishes it with
206+
<code>Atomics.compareExchange</code>. The consumer observes the slot with
207+
<code>Atomics.load</code>. Both sides run on one agent, which keeps worker
208+
scheduling out of the score.
209+
Source code: <a href="atomics/benchmark.js">atomics/benchmark.js</a>
210+
</dd>
200211
<dt id="babel-minify-wtb">babel-minify-wtb</dt>
201212
<dd>
202213
<a href="https://github.com/babel/minify">Babel Minify</a> is an ES2015+ aware minifier based on the

0 commit comments

Comments
 (0)