2026-07-30 15:13:53 +09:00
'use strict' ;
const fs = require ( 'fs' ) ;
const os = require ( 'os' ) ;
const path = require ( 'path' ) ;
const http = require ( 'http' ) ;
const crypto = require ( 'crypto' ) ;
const { spawn , spawnSync } = require ( 'child_process' ) ;
const { assert , root } = require ( './helpers/app-source' ) ;
const browserPath = process . env . BEND _FIELD _BROWSER _PATH || process . env . BEND _FIELD _EDGE _PATH || (
process . platform === 'win32' ? 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' : '/usr/bin/chromium'
) ;
const requested = ( process . env . BEND _FIELD _SCALE _SIZES || '10000,100000,200000' ) . split ( ',' ) . map ( Number ) . filter ( value => Number . isSafeInteger ( value ) && value > 0 && value <= 200000 ) ;
if ( ! requested . length ) throw new Error ( 'BEND_FIELD_SCALE_SIZES did not contain a supported board count.' ) ;
2026-08-01 16:06:14 +09:00
const worldDbName = 'bend-field:v30:linkfield-single-world-20260801:world' , explicitBenchmarkUrl = process . env . BEND _FIELD _BENCHMARK _URL || '' , benchmarkHost = process . env . BEND _FIELD _BENCHMARK _HOST || 'localhost' , useExtension = process . env . BEND _FIELD _BENCHMARK _EXTENSION === '1' ;
2026-07-30 15:13:53 +09:00
const debuggingPort = 22000 + Math . floor ( Math . random ( ) * 1000 ) , temporaryRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'bend-field-v2-scale-' ) ) , profilePath = path . join ( temporaryRoot , 'browser-profile' ) ;
const sleep = milliseconds => new Promise ( resolve => setTimeout ( resolve , milliseconds ) ) ;
function stopBrowserTree ( child ) {
if ( ! child ? . pid ) return ;
if ( process . platform === 'win32' ) {
const taskkill = path . join ( process . env . SystemRoot || 'C:\\Windows' , 'System32' , 'taskkill.exe' ) ,
result = spawnSync ( taskkill , [ '/pid' , String ( child . pid ) , '/T' , '/F' ] , { stdio : 'ignore' , windowsHide : true } ) ;
if ( result . status !== 0 && ! child . killed ) try { child . kill ( ) } catch ( _ ) { }
} else if ( ! child . killed ) child . kill ( 'SIGTERM' ) ;
}
function extensionIdFromPublicKey ( der ) {
const digest = crypto . createHash ( 'sha256' ) . update ( der ) . digest ( ) . subarray ( 0 , 16 ) ;
return [ ... digest ] . map ( byte => String . fromCharCode ( 97 + ( byte >> 4 ) , 97 + ( byte & 15 ) ) ) . join ( '' ) ;
}
function prepareBenchmarkExtension ( ) {
const directory = path . join ( temporaryRoot , 'extension' ) ; fs . mkdirSync ( directory , { recursive : true } ) ;
for ( const name of [ 'index.html' , 'style.css' , 'favicon.svg' , 'favicon.ico' , 'puzzle-core.js' , 'app-logic.js' , 'field-persistence.js' , 'field-persistence-worker.js' , 'app.js' , 'puzzle-worker.js' ] ) fs . copyFileSync ( path . join ( root , name ) , path . join ( directory , name ) ) ;
const { publicKey } = crypto . generateKeyPairSync ( 'rsa' , { modulusLength : 2048 } ) , der = publicKey . export ( { type : 'spki' , format : 'der' } ) , key = der . toString ( 'base64' ) , id = extensionIdFromPublicKey ( der ) ;
fs . writeFileSync ( path . join ( directory , 'manifest.json' ) , JSON . stringify ( { manifest _version : 3 , name : 'Bend Field V2 Storage Benchmark' , version : '47.36.0' , key , permissions : [ 'unlimitedStorage' ] } , null , 2 ) ) ;
return { directory , id , url : ` chrome-extension:// ${ id } /index.html ` } ;
}
class CdpClient {
constructor ( url ) { this . url = url ; this . sequence = 0 ; this . pending = new Map ( ) ; this . socket = null }
async connect ( ) {
this . socket = new WebSocket ( this . url ) ;
await new Promise ( ( resolve , reject ) => { const timer = setTimeout ( ( ) => reject ( new Error ( 'CDP connection timed out' ) ) , 15000 ) ; this . socket . addEventListener ( 'open' , ( ) => { clearTimeout ( timer ) ; resolve ( ) } , { once : true } ) ; this . socket . addEventListener ( 'error' , event => { clearTimeout ( timer ) ; reject ( event . error || new Error ( 'CDP connection failed' ) ) } , { once : true } ) } ) ;
this . socket . addEventListener ( 'message' , event => { const message = JSON . parse ( event . data ) ; if ( ! message . id ) return ; const pending = this . pending . get ( message . id ) ; if ( ! pending ) return ; this . pending . delete ( message . id ) ; if ( message . error ) pending . reject ( new Error ( ` ${ pending . method } : ${ message . error . message } ` ) ) ; else pending . resolve ( message . result ) } ) ;
}
send ( method , params = { } ) { const id = ++ this . sequence ; return new Promise ( ( resolve , reject ) => { this . pending . set ( id , { resolve , reject , method } ) ; this . socket . send ( JSON . stringify ( { id , method , params } ) ) } ) }
async evaluate ( expression ) { const response = await this . send ( 'Runtime.evaluate' , { expression , awaitPromise : true , returnByValue : true , userGesture : true } ) ; if ( response . exceptionDetails ) throw new Error ( response . exceptionDetails . exception ? . description || response . exceptionDetails . text || 'Browser evaluation failed' ) ; return response . result . value }
close ( ) { try { this . socket ? . close ( ) } catch ( _ ) { } }
}
async function waitFor ( check , { timeout = 30000 , interval = 50 , label = 'condition' } = { } ) { const deadline = Date . now ( ) + timeout ; let lastError = null ; while ( Date . now ( ) < deadline ) { try { const value = await check ( ) ; if ( value ) return value } catch ( error ) { lastError = error } await sleep ( interval ) } throw lastError || new Error ( ` Timed out waiting for ${ label } ` ) }
async function endpoint ( ) { return waitFor ( async ( ) => { const response = await fetch ( ` http://127.0.0.1: ${ debuggingPort } /json/list ` , { signal : AbortSignal . timeout ( 1500 ) } ) ; if ( ! response . ok ) return null ; return ( await response . json ( ) ) . find ( target => target . type === 'page' && target . webSocketDebuggerUrl ) } , { timeout : 20000 , label : 'browser DevTools endpoint' } ) }
async function startServer ( ) {
const types = { '.html' : 'text/html; charset=utf-8' , '.js' : 'text/javascript; charset=utf-8' , '.css' : 'text/css; charset=utf-8' , '.svg' : 'image/svg+xml' , '.ttf' : 'font/ttf' , '.ico' : 'image/x-icon' } ;
const server = http . createServer ( ( request , response ) => { const pathname = new URL ( request . url , 'http://localhost' ) . pathname , relative = pathname === '/' ? 'index.html' : decodeURIComponent ( pathname . slice ( 1 ) ) , file = path . resolve ( root , relative ) ; if ( file !== root && ! file . startsWith ( root + path . sep ) ) { response . writeHead ( 403 ) ; response . end ( ) ; return } fs . readFile ( file , ( error , body ) => { if ( error ) { response . writeHead ( error . code === 'ENOENT' ? 404 : 500 ) ; response . end ( ) ; return } response . writeHead ( 200 , { 'content-type' : types [ path . extname ( file ) ] || 'application/octet-stream' , 'cache-control' : 'no-store' } ) ; response . end ( body ) } ) } ) ;
await new Promise ( ( resolve , reject ) => { server . once ( 'error' , reject ) ; server . listen ( 0 , '127.0.0.1' , ( ) => { server . off ( 'error' , reject ) ; resolve ( ) } ) } ) ; return server ;
}
const transactionDoneSource = ` tx=>new Promise((resolve,reject)=>{tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error)}) ` ;
async function waitUntilReady ( client , timeout = 60000 ) { return waitFor ( ( ) => client . evaluate ( "document.body?.dataset?.ready==='true'&&globalThis.__bendStartupSnapshot" ) , { timeout , label : 'game startup' } ) }
async function waitForV2 ( client ) {
return waitFor ( ( ) => client . evaluate ( ` (async()=>{const db=await new Promise((resolve,reject)=>{const request=indexedDB.open( ${ JSON . stringify ( worldDbName ) } );request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});const tx=db.transaction('control','readonly'),request=tx.objectStore('control').get('active'),control=await new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});db.close();return control?.activeFormat===2&&control.activeEpoch})() ` ) , { timeout : 60000 , label : 'V2 storage migration' } ) ;
}
async function readStarter ( client ) {
return client . evaluate ( ` (async()=>{const done= ${ transactionDoneSource } ,read=request=>new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)}),db=await new Promise((resolve,reject)=>{const request=indexedDB.open( ${ JSON . stringify ( worldDbName ) } );request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});let tx=db.transaction('control','readonly'),control=await read(tx.objectStore('control').get('active'));await done(tx);const epoch=control.activeEpoch;tx=db.transaction(['worlds','boardIndex','boardPuzzles','boardStates'],'readonly');const[world,index,puzzle,state]=await Promise.all([read(tx.objectStore('worlds').get(epoch)),read(tx.objectStore('boardIndex').get([epoch,'B0'])),read(tx.objectStore('boardPuzzles').get([epoch,'B0'])),read(tx.objectStore('boardStates').get([epoch,'B0']))]);await done(tx);db.close();return{control,world,index,puzzle,state}})() ` ) ;
}
async function seedV2 ( client , starter , count ) {
const expression = ` (async()=>{
const starter = $ { JSON . stringify ( starter ) } , count = $ { count } , dbName = $ { JSON . stringify ( worldDbName ) } , batchSize = 4000 ,
done = $ { transactionDoneSource } , open = ( ) => new Promise ( ( resolve , reject ) => { const request = indexedDB . open ( dbName ) ; request . onsuccess = ( ) => resolve ( request . result ) ; request . onerror = ( ) => reject ( request . error ) } ) , db = await open ( ) , epoch = starter . control . activeEpoch ;
let tx = db . transaction ( [ 'boardIndex' , 'boardPuzzles' , 'boardStates' , 'worlds' ] , 'readwrite' ) ; tx . objectStore ( 'boardIndex' ) . clear ( ) ; tx . objectStore ( 'boardPuzzles' ) . clear ( ) ; tx . objectStore ( 'boardStates' ) . clear ( ) ; tx . objectStore ( 'worlds' ) . clear ( ) ; await done ( tx ) ;
const puzzleValue = starter . puzzle . puzzle , stateValue = starter . state . value , started = performance . now ( ) ;
for ( let offset = 0 ; offset < count ; offset += batchSize ) {
tx = db . transaction ( [ 'boardIndex' , 'boardPuzzles' , 'boardStates' ] , 'readwrite' ) ; const indexStore = tx . objectStore ( 'boardIndex' ) , puzzleStore = tx . objectStore ( 'boardPuzzles' ) , stateStore = tx . objectStore ( 'boardStates' ) , end = Math . min ( count , offset + batchSize ) ;
for ( let number = offset ; number < end ; number ++ ) {
const id = 'B' + number , x = number % 1000 , y = Math . floor ( number / 1000 ) , metaRev = 1000000 + number * 2 , stateRev = metaRev + 1 , author = 'scale-benchmark' ;
indexStore . put ( { epoch , id , number , x , y , chunks : [ [ 0 , 0 ] ] , level : 1 , targetLevel : 1 , seed : number + 1 , axis : starter . index . axis , entrySide : null , metaRev , stateRev , revAuthor : author , solved : false , expanded : false , scoreAwarded : 0 , hasProgress : false , specialFlags : { crossing : false , warp : false , lock : false } , shop : null } ) ;
puzzleStore . put ( { epoch , id , metaRev , revAuthor : author , generatorVersion : starter . puzzle . generatorVersion , puzzle : puzzleValue } ) ;
stateStore . put ( { epoch , id , stateRev , revAuthor : author , value : { ... stateValue , paths : [ ] , specialProgress : { crossings : [ ] } , solved : false , expanded : false , scoreAwarded : 0 , store : null , rev : stateRev , revAuthor : author } } ) ;
}
await done ( tx ) ; await new Promise ( resolve => setTimeout ( resolve , 0 ) ) ;
}
const global = { ... starter . world . global , worldEpoch : epoch , nextId : count , solved : 0 , score : 0 , globalRev : ( starter . world . global . globalRev || 0 ) + count + 1 , globalRevAuthor : 'scale-benchmark' , selectedBoardId : 'B0' , cameraAnchor : null , updatedAt : Date . now ( ) } , world = { ... starter . world , epoch , status : 'active' , global , boardCount : count , solvedCount : 0 , score : 0 , bounds : { minX : 0 , minY : 0 , maxX : Math . min ( 1000 , count ) , maxY : Math . ceil ( count / 1000 ) } , approximateBytes : count * 2048 , source : { kind : 'benchmark' } , progress : null } ;
tx = db . transaction ( [ 'control' , 'worlds' ] , 'readwrite' ) ; tx . objectStore ( 'worlds' ) . put ( world ) ; tx . objectStore ( 'control' ) . put ( { ... starter . control , key : 'active' , activeFormat : 2 , activeEpoch : epoch , previousEpoch : undefined , activationVerified : true } ) ; await done ( tx ) ; db . close ( ) ; localStorage . clear ( ) ; sessionStorage . clear ( ) ; return { seedMs : performance . now ( ) - started , count } ;
} ) ( ) ` ;
const result = await client . evaluate ( expression ) ; assert ( result ? . count === count , ` Only ${ result ? . count || 0 } of ${ count } V2 boards were seeded ` ) ; return result ;
}
async function measure ( client , count ) {
const navigationStarted = Date . now ( ) ; await client . send ( 'Page.reload' , { ignoreCache : true } ) ; const startup = await waitUntilReady ( client , 180000 ) , readyMs = Date . now ( ) - navigationStarted ;
const snapshot = await client . evaluate ( ` (()=>({capture:globalThis.__bendStartupSnapshot,ready:document.body.dataset.ready,loaded:fieldIndexLoadedCount,expected:fieldIndexExpectedCount,complete:fieldIndexComplete,b0:Boolean(data?.metas?.B0?.puzzle),navigation:performance.getEntriesByType('navigation')[0]?.duration||0,memory:performance.memory?.usedJSHeapSize||null}))() ` ) ;
assert ( snapshot . ready === 'true' && snapshot . b0 , ` ${ count } board V2 startup did not hydrate B0 ` ) ; assert ( snapshot . capture . expected === count , ` ${ count } board V2 startup declared ${ snapshot . capture . expected } boards ` ) ; assert ( snapshot . capture . loaded <= 512 , ` ${ count } board V2 startup eagerly loaded ${ snapshot . capture . loaded } board indexes ` ) ; assert ( snapshot . capture . complete === false , ` ${ count } board V2 startup completed the full index scan before first paint ` ) ;
const scanStarted = Date . now ( ) ; await waitFor ( ( ) => client . evaluate ( 'fieldIndexComplete===true&&fieldIndexLoadedCount===fieldIndexExpectedCount' ) , { timeout : 600000 , interval : 100 , label : ` ${ count } board index scan ` } ) ; const scanMs = Date . now ( ) - scanStarted ;
const final = await client . evaluate ( ` ({loaded:fieldIndexLoadedCount,expected:fieldIndexExpectedCount,metas:Object.keys(data.metas).length,memory:performance.memory?.usedJSHeapSize||null}) ` ) ; assert ( final . loaded === count && final . metas === count , ` ${ count } board V2 background index scan was incomplete ` ) ;
return { boards : count , readyMs , navigationMs : snapshot . navigation , capturedReadyMs : snapshot . capture . at , initialIndexes : snapshot . capture . loaded , scanMs , heapAtReady : snapshot . memory , heapAfterScan : final . memory } ;
}
async function main ( ) {
if ( ! fs . existsSync ( browserPath ) ) throw new Error ( ` Browser was not found at ${ browserPath } ` ) ;
const extension = useExtension ? prepareBenchmarkExtension ( ) : null ; console . log ( ` Launching ${ browserPath } ` ) ; const server = await startServer ( ) , port = server . address ( ) . port , args = [ '--headless' , '--no-first-run' , '--no-sandbox' , '--enable-unsafe-swiftshader' , '--allow-file-access-from-files' , '--disable-dev-shm-usage' , '--disable-background-networking' , ` --remote-debugging-port= ${ debuggingPort } ` , '--remote-allow-origins=*' , ` --user-data-dir= ${ profilePath } ` , '--window-size=1280,900' , 'about:blank' ] ; if ( extension ) { args . splice ( - 1 , 0 , ` --disable-extensions-except= ${ extension . directory } ` , ` --load-extension= ${ extension . directory } ` ) } const browser = spawn ( browserPath , args , { stdio : 'ignore' } ) ; let client = null ;
try {
console . log ( 'Waiting for DevTools' ) ; const target = await endpoint ( ) ; console . log ( 'Connecting to page' ) ; client = new CdpClient ( target . webSocketDebuggerUrl ) ; await client . connect ( ) ; await client . send ( 'Page.enable' ) ; await client . send ( 'Runtime.enable' ) ;
await client . send ( 'Page.addScriptToEvaluateOnNewDocument' , { source : ` (()=>{const capture=()=>{if(document.body?.dataset?.ready==='true'&&!globalThis.__bendStartupSnapshot)globalThis.__bendStartupSnapshot={at:performance.now(),loaded:typeof fieldIndexLoadedCount==='number'?fieldIndexLoadedCount:null,expected:typeof fieldIndexExpectedCount==='number'?fieldIndexExpectedCount:null,complete:typeof fieldIndexComplete==='boolean'?fieldIndexComplete:null};};const observe=()=>{if(document.documentElement)new MutationObserver(capture).observe(document.documentElement,{subtree:true,attributes:true,attributeFilter:['data-ready']});capture();};if(document.documentElement)observe();else addEventListener('DOMContentLoaded',observe,{once:true})})(); ` } ) ;
const benchmarkUrl = explicitBenchmarkUrl || ( extension ? extension . url : ` http:// ${ benchmarkHost } : ${ port } / ` ) ; console . log ( ` Navigating to ${ benchmarkUrl } ` ) ; await client . send ( 'Page.navigate' , { url : benchmarkUrl } ) ; try { await waitUntilReady ( client , 30000 ) } catch ( error ) { const state = await client . evaluate ( "({href:location.href,ready:document.body?.dataset?.ready,error:document.body?.dataset?.error,status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,800)||'',scripts:[...document.scripts].map(s=>s.src)})" ) . catch ( ( ) => null ) ; throw new Error ( ` ${ error . message } : ${ JSON . stringify ( state ) } ` ) } console . log ( 'Game ready; waiting for V2' ) ; await waitForV2 ( client ) ; console . log ( 'V2 active; reading starter' ) ; const starter = await readStarter ( client ) ; assert ( starter ? . index ? . id === 'B0' && starter ? . puzzle ? . puzzle && starter ? . state ? . value , 'Could not read the V2 starter board' ) ;
const results = [ ] ; for ( const count of requested ) { console . log ( ` Seeding ${ count . toLocaleString ( ) } boards ` ) ; const seeded = await seedV2 ( client , starter , count ) , measured = await measure ( client , count ) , result = { ... measured , seedMs : seeded . seedMs } ; results . push ( result ) ; console . log ( ` ${ count . toLocaleString ( ) } boards | seed ${ ( seeded . seedMs / 1000 ) . toFixed ( 1 ) } s | ready ${ measured . readyMs } ms ( ${ measured . initialIndexes } indexes) | full scan ${ ( measured . scanMs / 1000 ) . toFixed ( 1 ) } s ` ) }
console . log ( ` FIELD_STORAGE_BENCHMARK_JSON= ${ JSON . stringify ( results ) } ` ) ; console . log ( 'V2 field storage scale benchmark passed' ) ;
} finally {
client ? . close ( ) ; stopBrowserTree ( browser ) ;
server . closeAllConnections ? . ( ) ; await new Promise ( resolve => server . close ( ( ) => resolve ( ) ) ) ; for ( let attempt = 0 ; attempt < 8 ; attempt ++ ) { try { fs . rmSync ( temporaryRoot , { recursive : true , force : true } ) ; break } catch ( error ) { if ( attempt === 7 ) console . warn ( ` Benchmark cleanup deferred: ${ error . message } ` ) ; await sleep ( 200 ) } }
}
}
main ( ) . then ( ( ) => process . exit ( 0 ) ) . catch ( error => { console . error ( error ) ; process . exit ( 1 ) } ) ;