mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Compare commits
6 Commits
claude/fix
...
claude/exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59199bfd73 | ||
|
|
06b3e11a62 | ||
|
|
24546b86b7 | ||
|
|
73974d1378 | ||
|
|
f73ab35e8c | ||
|
|
c22ced8b9c |
194
benchmark-final.js
Normal file
194
benchmark-final.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// Final benchmark: processStorage + Atomics vs postMessage
|
||||
const NUM_WORKERS = 5;
|
||||
const NUM_MESSAGES = 1000;
|
||||
const MESSAGE_SIZE = 1024;
|
||||
|
||||
const measureMemory = () => {
|
||||
if (typeof Bun !== 'undefined' && Bun.gc) {
|
||||
Bun.gc(true);
|
||||
}
|
||||
return process.memoryUsage();
|
||||
};
|
||||
|
||||
// Benchmark processStorage + Atomics
|
||||
const benchmarkProcessStorage = async () => {
|
||||
console.log("\n=== ProcessStorage + Atomics Benchmark ===");
|
||||
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
const sharedBuffer = new SharedArrayBuffer(8 * NUM_WORKERS);
|
||||
const counters = new Int32Array(sharedBuffer);
|
||||
|
||||
const startMemory = measureMemory();
|
||||
|
||||
// Pre-populate storage
|
||||
const testData = 'x'.repeat(MESSAGE_SIZE);
|
||||
for (let i = 0; i < NUM_MESSAGES; i++) {
|
||||
storage.setItem(`msg_${i}`, testData);
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Create workers
|
||||
const workers = [];
|
||||
const promises = [];
|
||||
|
||||
for (let w = 0; w < NUM_WORKERS; w++) {
|
||||
const worker = new Worker("./benchmark-worker-ps.js");
|
||||
workers.push(worker);
|
||||
|
||||
const promise = new Promise(resolve => {
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data.type === 'done') {
|
||||
resolve(e.data);
|
||||
}
|
||||
};
|
||||
});
|
||||
promises.push(promise);
|
||||
|
||||
worker.postMessage({
|
||||
type: 'start',
|
||||
workerId: w,
|
||||
numMessages: NUM_MESSAGES,
|
||||
sharedBuffer
|
||||
});
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = performance.now();
|
||||
const endMemory = measureMemory();
|
||||
|
||||
workers.forEach(w => w.terminate());
|
||||
storage.clear();
|
||||
|
||||
const totalTime = endTime - startTime;
|
||||
const totalProcessed = results.reduce((sum, r) => sum + r.processed, 0);
|
||||
|
||||
return {
|
||||
method: 'processStorage + Atomics',
|
||||
totalTime,
|
||||
totalProcessed,
|
||||
throughput: totalProcessed / (totalTime / 1000),
|
||||
memoryDelta: endMemory.rss - startMemory.rss,
|
||||
results
|
||||
};
|
||||
};
|
||||
|
||||
// Benchmark postMessage
|
||||
const benchmarkPostMessage = async () => {
|
||||
console.log("\n=== PostMessage Benchmark ===");
|
||||
|
||||
const startMemory = measureMemory();
|
||||
const testData = 'x'.repeat(MESSAGE_SIZE);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Create workers
|
||||
const workers = [];
|
||||
const promises = [];
|
||||
|
||||
for (let w = 0; w < NUM_WORKERS; w++) {
|
||||
const worker = new Worker("./benchmark-worker-pm.js");
|
||||
workers.push(worker);
|
||||
|
||||
const promise = new Promise(resolve => {
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data.type === 'done') {
|
||||
resolve(e.data);
|
||||
}
|
||||
};
|
||||
});
|
||||
promises.push(promise);
|
||||
|
||||
// Start worker
|
||||
worker.postMessage({
|
||||
type: 'start',
|
||||
numMessages: NUM_MESSAGES
|
||||
});
|
||||
|
||||
// Send messages
|
||||
for (let i = 0; i < NUM_MESSAGES; i++) {
|
||||
worker.postMessage({
|
||||
type: 'message',
|
||||
workerId: w,
|
||||
data: testData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = performance.now();
|
||||
const endMemory = measureMemory();
|
||||
|
||||
workers.forEach(w => w.terminate());
|
||||
|
||||
const totalTime = endTime - startTime;
|
||||
const totalProcessed = results.reduce((sum, r) => sum + r.processed, 0);
|
||||
|
||||
return {
|
||||
method: 'postMessage',
|
||||
totalTime,
|
||||
totalProcessed,
|
||||
throughput: totalProcessed / (totalTime / 1000),
|
||||
memoryDelta: endMemory.rss - startMemory.rss,
|
||||
results
|
||||
};
|
||||
};
|
||||
|
||||
// Run benchmark
|
||||
const runBenchmark = async () => {
|
||||
console.log(`🚀 ProcessStorage vs PostMessage Benchmark`);
|
||||
console.log(`Configuration:`);
|
||||
console.log(`- Workers: ${NUM_WORKERS}`);
|
||||
console.log(`- Messages per worker: ${NUM_MESSAGES}`);
|
||||
console.log(`- Message size: ${MESSAGE_SIZE} bytes`);
|
||||
console.log(`- Total messages: ${NUM_MESSAGES * NUM_WORKERS}`);
|
||||
|
||||
try {
|
||||
const psResult = await benchmarkProcessStorage();
|
||||
await new Promise(r => setTimeout(r, 2000)); // Cool down
|
||||
const pmResult = await benchmarkPostMessage();
|
||||
|
||||
console.log(`\n📊 Results Summary:`);
|
||||
console.log(`┌─────────────────────────────┬──────────────────┬──────────────────┐`);
|
||||
console.log(`│ Method │ processStorage │ postMessage │`);
|
||||
console.log(`├─────────────────────────────┼──────────────────┼──────────────────┤`);
|
||||
console.log(`│ Total Time (ms) │ ${psResult.totalTime.toFixed(2).padStart(16)} │ ${pmResult.totalTime.toFixed(2).padStart(16)} │`);
|
||||
console.log(`│ Throughput (msgs/sec) │ ${psResult.throughput.toFixed(0).padStart(16)} │ ${pmResult.throughput.toFixed(0).padStart(16)} │`);
|
||||
console.log(`│ Memory Delta (KB) │ ${(psResult.memoryDelta/1024).toFixed(1).padStart(16)} │ ${(pmResult.memoryDelta/1024).toFixed(1).padStart(16)} │`);
|
||||
console.log(`│ Messages Processed │ ${psResult.totalProcessed.toString().padStart(16)} │ ${pmResult.totalProcessed.toString().padStart(16)} │`);
|
||||
console.log(`└─────────────────────────────┴──────────────────┴──────────────────┘`);
|
||||
|
||||
const speedupRatio = pmResult.totalTime / psResult.totalTime;
|
||||
const memoryRatio = psResult.memoryDelta / pmResult.memoryDelta;
|
||||
|
||||
console.log(`\n🎯 Performance Analysis:`);
|
||||
if (speedupRatio > 1.1) {
|
||||
console.log(`✅ processStorage is ${speedupRatio.toFixed(2)}x faster than postMessage`);
|
||||
} else if (speedupRatio < 0.9) {
|
||||
console.log(`❌ processStorage is ${(1/speedupRatio).toFixed(2)}x slower than postMessage`);
|
||||
} else {
|
||||
console.log(`🟡 Similar performance (${speedupRatio.toFixed(2)}x)`);
|
||||
}
|
||||
|
||||
if (memoryRatio > 1.1) {
|
||||
console.log(`✅ processStorage uses ${memoryRatio.toFixed(2)}x less memory than postMessage`);
|
||||
} else if (memoryRatio < 0.9) {
|
||||
console.log(`❌ processStorage uses ${(1/memoryRatio).toFixed(2)}x more memory than postMessage`);
|
||||
} else {
|
||||
console.log(`🟡 Similar memory usage (${memoryRatio.toFixed(2)}x)`);
|
||||
}
|
||||
|
||||
console.log(`\n💡 Use Cases Where processStorage Excels:`);
|
||||
console.log(`- Shared configuration/state across workers`);
|
||||
console.log(`- Caching expensive computations`);
|
||||
console.log(`- Real-time coordination with Atomics`);
|
||||
console.log(`- Zero-copy string sharing between threads`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Benchmark failed:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
runBenchmark();
|
||||
24
benchmark-worker-pm.js
Normal file
24
benchmark-worker-pm.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// PostMessage worker
|
||||
let processed = 0;
|
||||
let numMessages = 0;
|
||||
let startTime = 0;
|
||||
|
||||
onmessage = (e) => {
|
||||
if (e.data.type === 'start') {
|
||||
numMessages = e.data.numMessages;
|
||||
startTime = performance.now();
|
||||
processed = 0;
|
||||
} else if (e.data.type === 'message') {
|
||||
processed++;
|
||||
|
||||
if (processed >= numMessages) {
|
||||
const endTime = performance.now();
|
||||
postMessage({
|
||||
type: 'done',
|
||||
workerId: e.data.workerId || 0,
|
||||
processed,
|
||||
duration: endTime - startTime
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
30
benchmark-worker-ps.js
Normal file
30
benchmark-worker-ps.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// ProcessStorage + Atomics worker
|
||||
const storage = Bun.experimental_processStorage;
|
||||
|
||||
onmessage = (e) => {
|
||||
if (e.data.type === 'start') {
|
||||
const { workerId, numMessages, sharedBuffer } = e.data;
|
||||
const counters = new Int32Array(sharedBuffer);
|
||||
|
||||
let processed = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
// Read messages from storage
|
||||
for (let i = 0; i < numMessages; i++) {
|
||||
const data = storage.getItem(`msg_${i}`);
|
||||
if (data) {
|
||||
processed++;
|
||||
Atomics.add(counters, workerId, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
|
||||
postMessage({
|
||||
type: 'done',
|
||||
workerId,
|
||||
processed,
|
||||
duration: endTime - startTime
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -23,6 +23,7 @@ src/bun.js/bindings/BunJSCEventLoop.cpp
|
||||
src/bun.js/bindings/BunObject.cpp
|
||||
src/bun.js/bindings/BunPlugin.cpp
|
||||
src/bun.js/bindings/BunProcess.cpp
|
||||
src/bun.js/bindings/BunProcessStorage.cpp
|
||||
src/bun.js/bindings/BunString.cpp
|
||||
src/bun.js/bindings/BunWorkerGlobalScope.cpp
|
||||
src/bun.js/bindings/c-bindings.cpp
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "BunObjectModule.h"
|
||||
#include "JSCookie.h"
|
||||
#include "JSCookieMap.h"
|
||||
#include "BunProcessStorage.h"
|
||||
|
||||
#ifdef WIN32
|
||||
#include <ws2def.h>
|
||||
@@ -792,6 +793,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
zstdDecompressSync BunObject_callback_zstdDecompressSync DontDelete|Function 1
|
||||
zstdCompress BunObject_callback_zstdCompress DontDelete|Function 1
|
||||
zstdDecompress BunObject_callback_zstdDecompress DontDelete|Function 1
|
||||
experimental_processStorage constructProcessStorageObject DontDelete|PropertyCallback
|
||||
@end
|
||||
*/
|
||||
|
||||
|
||||
269
src/bun.js/bindings/BunProcessStorage.cpp
Normal file
269
src/bun.js/bindings/BunProcessStorage.cpp
Normal file
@@ -0,0 +1,269 @@
|
||||
#include "root.h"
|
||||
#include "helpers.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include <JavaScriptCore/ArgList.h>
|
||||
#include <JavaScriptCore/JSFunction.h>
|
||||
#include <JavaScriptCore/JSObject.h>
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
#include <JavaScriptCore/JSString.h>
|
||||
#include <JavaScriptCore/CallFrame.h>
|
||||
#include <wtf/HashMap.h>
|
||||
#include <wtf/Lock.h>
|
||||
#include <wtf/text/WTFString.h>
|
||||
#include <wtf/StdLibExtras.h>
|
||||
|
||||
using namespace JSC;
|
||||
using namespace WebCore;
|
||||
|
||||
namespace Bun {
|
||||
|
||||
// Thread-safe process storage implementation
|
||||
class ProcessStorage {
|
||||
public:
|
||||
static ProcessStorage& getInstance()
|
||||
{
|
||||
static std::once_flag s_onceFlag;
|
||||
static ProcessStorage* s_instance = nullptr;
|
||||
|
||||
std::call_once(s_onceFlag, []() {
|
||||
s_instance = new ProcessStorage();
|
||||
});
|
||||
|
||||
return *s_instance;
|
||||
}
|
||||
|
||||
void setItem(const String& key, const String& value)
|
||||
{
|
||||
Locker locker { m_lock };
|
||||
m_storage.set(key.isolatedCopy(), value.isolatedCopy());
|
||||
}
|
||||
|
||||
String getItem(const String& key)
|
||||
{
|
||||
Locker locker { m_lock };
|
||||
auto it = m_storage.find(key);
|
||||
if (it != m_storage.end()) {
|
||||
return it->value;
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
bool removeItem(const String& key)
|
||||
{
|
||||
Locker locker { m_lock };
|
||||
return m_storage.remove(key);
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
Locker locker { m_lock };
|
||||
m_storage.clear();
|
||||
}
|
||||
|
||||
String getOrSetItem(const String& key, const String& defaultValue)
|
||||
{
|
||||
Locker locker { m_lock };
|
||||
auto it = m_storage.find(key);
|
||||
if (it != m_storage.end()) {
|
||||
return it->value;
|
||||
}
|
||||
// Item doesn't exist, set it and return the value
|
||||
String isolatedKey = key.isolatedCopy();
|
||||
String isolatedValue = defaultValue.isolatedCopy();
|
||||
m_storage.set(isolatedKey, isolatedValue);
|
||||
return isolatedValue;
|
||||
}
|
||||
|
||||
String takeItem(const String& key)
|
||||
{
|
||||
Locker locker { m_lock };
|
||||
auto it = m_storage.find(key);
|
||||
if (it != m_storage.end()) {
|
||||
String value = it->value;
|
||||
m_storage.remove(it);
|
||||
return value;
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
private:
|
||||
ProcessStorage() = default;
|
||||
~ProcessStorage() = default;
|
||||
ProcessStorage(const ProcessStorage&) = delete;
|
||||
ProcessStorage& operator=(const ProcessStorage&) = delete;
|
||||
|
||||
WTF_GUARDED_BY_LOCK(m_lock)
|
||||
HashMap<String, String> m_storage;
|
||||
Lock m_lock;
|
||||
};
|
||||
|
||||
// JSFunction implementations
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessStorageGetItem, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 1) {
|
||||
throwTypeError(globalObject, scope, "getItem requires 1 argument"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSValue keyValue = callFrame->uncheckedArgument(0);
|
||||
if (keyValue.isUndefinedOrNull()) {
|
||||
return JSValue::encode(jsNull());
|
||||
}
|
||||
|
||||
String key = keyValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
String value = ProcessStorage::getInstance().getItem(key);
|
||||
if (value.isNull()) {
|
||||
return JSValue::encode(jsNull());
|
||||
}
|
||||
|
||||
return JSValue::encode(jsString(vm, value));
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessStorageSetItem, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 2) {
|
||||
throwTypeError(globalObject, scope, "setItem requires 2 arguments"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSValue keyValue = callFrame->uncheckedArgument(0);
|
||||
JSValue valueValue = callFrame->uncheckedArgument(1);
|
||||
|
||||
String key = keyValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
String value = valueValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
ProcessStorage::getInstance().setItem(key, value);
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessStorageRemoveItem, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 1) {
|
||||
throwTypeError(globalObject, scope, "removeItem requires 1 argument"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSValue keyValue = callFrame->uncheckedArgument(0);
|
||||
if (keyValue.isUndefinedOrNull()) {
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
String key = keyValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
ProcessStorage::getInstance().removeItem(key);
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessStorageClear, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
ProcessStorage::getInstance().clear();
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessStorageGetOrSetItem, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 2) {
|
||||
throwTypeError(globalObject, scope, "getOrSetItem requires 2 arguments"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSValue keyValue = callFrame->uncheckedArgument(0);
|
||||
JSValue defaultValue = callFrame->uncheckedArgument(1);
|
||||
|
||||
String key = keyValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
String defaultString = defaultValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
String result = ProcessStorage::getInstance().getOrSetItem(key, defaultString);
|
||||
|
||||
return JSValue::encode(jsString(vm, result));
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessStorageTakeItem, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (callFrame->argumentCount() < 1) {
|
||||
throwTypeError(globalObject, scope, "takeItem requires 1 argument"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSValue keyValue = callFrame->uncheckedArgument(0);
|
||||
if (keyValue.isUndefinedOrNull()) {
|
||||
return JSValue::encode(jsNull());
|
||||
}
|
||||
|
||||
String key = keyValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
String value = ProcessStorage::getInstance().takeItem(key);
|
||||
if (value.isNull()) {
|
||||
return JSValue::encode(jsNull());
|
||||
}
|
||||
|
||||
return JSValue::encode(jsString(vm, value));
|
||||
}
|
||||
|
||||
// Function to create the processStorage object
|
||||
JSValue constructProcessStorageObject(VM& vm, JSObject* bunObject)
|
||||
{
|
||||
JSGlobalObject* globalObject = bunObject->globalObject();
|
||||
JSC::JSObject* processStorageObject = JSC::constructEmptyObject(globalObject);
|
||||
|
||||
processStorageObject->putDirectNativeFunction(vm, globalObject,
|
||||
JSC::Identifier::fromString(vm, "getItem"_s), 1,
|
||||
jsFunctionProcessStorageGetItem, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
processStorageObject->putDirectNativeFunction(vm, globalObject,
|
||||
JSC::Identifier::fromString(vm, "setItem"_s), 2,
|
||||
jsFunctionProcessStorageSetItem, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
processStorageObject->putDirectNativeFunction(vm, globalObject,
|
||||
JSC::Identifier::fromString(vm, "removeItem"_s), 1,
|
||||
jsFunctionProcessStorageRemoveItem, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
processStorageObject->putDirectNativeFunction(vm, globalObject,
|
||||
JSC::Identifier::fromString(vm, "clear"_s), 0,
|
||||
jsFunctionProcessStorageClear, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
processStorageObject->putDirectNativeFunction(vm, globalObject,
|
||||
JSC::Identifier::fromString(vm, "getOrSetItem"_s), 2,
|
||||
jsFunctionProcessStorageGetOrSetItem, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
processStorageObject->putDirectNativeFunction(vm, globalObject,
|
||||
JSC::Identifier::fromString(vm, "takeItem"_s), 1,
|
||||
jsFunctionProcessStorageTakeItem, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
return processStorageObject;
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
11
src/bun.js/bindings/BunProcessStorage.h
Normal file
11
src/bun.js/bindings/BunProcessStorage.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace JSC {
|
||||
class JSValue;
|
||||
class VM;
|
||||
class JSObject;
|
||||
}
|
||||
|
||||
namespace Bun {
|
||||
JSC::JSValue constructProcessStorageObject(JSC::VM& vm, JSC::JSObject* bunObject);
|
||||
}
|
||||
253
test/js/bun/process-storage.test.ts
Normal file
253
test/js/bun/process-storage.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
test("Bun.experimental_processStorage basic functionality", () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
|
||||
// Clear any existing data
|
||||
storage.clear();
|
||||
|
||||
// Test setItem and getItem
|
||||
storage.setItem("test-key", "test-value");
|
||||
expect(storage.getItem("test-key")).toBe("test-value");
|
||||
|
||||
// Test null return for non-existent key
|
||||
expect(storage.getItem("non-existent")).toBe(null);
|
||||
|
||||
// Test removeItem
|
||||
storage.setItem("to-remove", "will-be-removed");
|
||||
expect(storage.getItem("to-remove")).toBe("will-be-removed");
|
||||
storage.removeItem("to-remove");
|
||||
expect(storage.getItem("to-remove")).toBe(null);
|
||||
|
||||
// Test clear
|
||||
storage.setItem("key1", "value1");
|
||||
storage.setItem("key2", "value2");
|
||||
storage.clear();
|
||||
expect(storage.getItem("key1")).toBe(null);
|
||||
expect(storage.getItem("key2")).toBe(null);
|
||||
});
|
||||
|
||||
test("Bun.experimental_processStorage string conversion", () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
// Test with numbers
|
||||
storage.setItem("number", 42);
|
||||
expect(storage.getItem("number")).toBe("42");
|
||||
|
||||
// Test with boolean
|
||||
storage.setItem("bool", true);
|
||||
expect(storage.getItem("bool")).toBe("true");
|
||||
|
||||
// Test with object (toString conversion)
|
||||
storage.setItem("object", { key: "value" });
|
||||
expect(storage.getItem("object")).toBe("[object Object]");
|
||||
|
||||
storage.clear();
|
||||
});
|
||||
|
||||
test("Bun.experimental_processStorage edge cases", () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
// Test with empty string
|
||||
storage.setItem("empty", "");
|
||||
expect(storage.getItem("empty")).toBe("");
|
||||
|
||||
// Test with spaces
|
||||
storage.setItem("spaces", " value with spaces ");
|
||||
expect(storage.getItem("spaces")).toBe(" value with spaces ");
|
||||
|
||||
// Test with special characters
|
||||
storage.setItem("special", "value\nwith\tspecial\rchars");
|
||||
expect(storage.getItem("special")).toBe("value\nwith\tspecial\rchars");
|
||||
|
||||
// Test with unicode
|
||||
storage.setItem("unicode", "🔥💯✨");
|
||||
expect(storage.getItem("unicode")).toBe("🔥💯✨");
|
||||
|
||||
storage.clear();
|
||||
});
|
||||
|
||||
test("Bun.experimental_processStorage process isolation", async () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
// Set some data in current process
|
||||
storage.setItem("current-process-key", "current-process-value");
|
||||
|
||||
const dir = tempDirWithFiles("process-storage-test", {
|
||||
"test.js": `
|
||||
const storage = Bun.experimental_processStorage;
|
||||
console.log(JSON.stringify({
|
||||
// Should be null since this is a separate process
|
||||
currentProcessValue: storage.getItem("current-process-key"),
|
||||
// Should work within this process
|
||||
newValue: (() => {
|
||||
storage.setItem("new-key", "new-value");
|
||||
return storage.getItem("new-key");
|
||||
})()
|
||||
}));
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
|
||||
const result = JSON.parse(stdout.trim());
|
||||
// Process storage is isolated per process
|
||||
expect(result.currentProcessValue).toBe(null);
|
||||
// But works within the same process
|
||||
expect(result.newValue).toBe("new-value");
|
||||
|
||||
// Verify our current process still has its data
|
||||
expect(storage.getItem("current-process-key")).toBe("current-process-value");
|
||||
// But not the data from the subprocess
|
||||
expect(storage.getItem("new-key")).toBe(null);
|
||||
|
||||
storage.clear();
|
||||
});
|
||||
|
||||
test("Bun.experimental_processStorage concurrent access", async () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
const dir = tempDirWithFiles("process-storage-concurrent", {
|
||||
"worker.js": `
|
||||
onmessage = (event) => {
|
||||
if (event.data === "start") {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
|
||||
// Read existing value
|
||||
const existing = storage.getItem("shared-key");
|
||||
|
||||
// Set worker-specific value
|
||||
storage.setItem("worker-key", "worker-value");
|
||||
|
||||
// Modify shared value
|
||||
storage.setItem("shared-key", "modified-by-worker");
|
||||
|
||||
postMessage({
|
||||
existing,
|
||||
workerValue: storage.getItem("worker-key"),
|
||||
sharedValue: storage.getItem("shared-key")
|
||||
});
|
||||
}
|
||||
};
|
||||
`,
|
||||
"main.js": `
|
||||
const storage = Bun.experimental_processStorage;
|
||||
|
||||
// Set initial value
|
||||
storage.setItem("shared-key", "initial-value");
|
||||
|
||||
const worker = new Worker("./worker.js");
|
||||
|
||||
worker.postMessage("start");
|
||||
|
||||
const result = await new Promise(resolve => {
|
||||
worker.onmessage = (event) => {
|
||||
resolve(event.data);
|
||||
};
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({
|
||||
workerSawInitial: result.existing,
|
||||
workerSetValue: result.workerValue,
|
||||
workerModifiedShared: result.sharedValue,
|
||||
mainSeesWorkerKey: storage.getItem("worker-key"),
|
||||
mainSeesSharedModification: storage.getItem("shared-key")
|
||||
}));
|
||||
|
||||
worker.terminate();
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
|
||||
const result = JSON.parse(stdout.trim());
|
||||
expect(result.workerSawInitial).toBe("initial-value");
|
||||
expect(result.workerSetValue).toBe("worker-value");
|
||||
expect(result.workerModifiedShared).toBe("modified-by-worker");
|
||||
expect(result.mainSeesWorkerKey).toBe("worker-value");
|
||||
expect(result.mainSeesSharedModification).toBe("modified-by-worker");
|
||||
|
||||
storage.clear();
|
||||
});
|
||||
|
||||
test("Bun.experimental_processStorage getOrSetItem", () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
// Test setting a new item
|
||||
const result1 = storage.getOrSetItem("new-key", "default-value");
|
||||
expect(result1).toBe("default-value");
|
||||
expect(storage.getItem("new-key")).toBe("default-value");
|
||||
|
||||
// Test getting an existing item (should not overwrite)
|
||||
storage.setItem("existing-key", "existing-value");
|
||||
const result2 = storage.getOrSetItem("existing-key", "new-default");
|
||||
expect(result2).toBe("existing-value");
|
||||
expect(storage.getItem("existing-key")).toBe("existing-value");
|
||||
|
||||
// Test with type conversion
|
||||
const result3 = storage.getOrSetItem("number-key", 42);
|
||||
expect(result3).toBe("42");
|
||||
expect(storage.getItem("number-key")).toBe("42");
|
||||
|
||||
storage.clear();
|
||||
});
|
||||
|
||||
test("Bun.experimental_processStorage takeItem", () => {
|
||||
const storage = Bun.experimental_processStorage;
|
||||
storage.clear();
|
||||
|
||||
// Test taking a non-existent item
|
||||
const result1 = storage.takeItem("non-existent");
|
||||
expect(result1).toBe(null);
|
||||
|
||||
// Test taking an existing item
|
||||
storage.setItem("to-take", "take-me");
|
||||
const result2 = storage.takeItem("to-take");
|
||||
expect(result2).toBe("take-me");
|
||||
|
||||
// Verify item was removed
|
||||
expect(storage.getItem("to-take")).toBe(null);
|
||||
|
||||
// Test taking the same item again (should be null)
|
||||
const result3 = storage.takeItem("to-take");
|
||||
expect(result3).toBe(null);
|
||||
|
||||
// Test with multiple items
|
||||
storage.setItem("item1", "value1");
|
||||
storage.setItem("item2", "value2");
|
||||
|
||||
const taken1 = storage.takeItem("item1");
|
||||
expect(taken1).toBe("value1");
|
||||
expect(storage.getItem("item1")).toBe(null);
|
||||
expect(storage.getItem("item2")).toBe("value2"); // Should still exist
|
||||
|
||||
storage.clear();
|
||||
});
|
||||
Reference in New Issue
Block a user