51 lines
7.1 KiB
JavaScript
51 lines
7.1 KiB
JavaScript
'use strict';
|
|
const {assert,app,css,functionSource,read}=require('./helpers/app-source');
|
|
const browserBenchmark=read('test/browser-performance-benchmark.js');
|
|
|
|
const scheduler=functionSource('scheduleReactionRender');
|
|
assert(app.includes('REACTION_TARGET_FPS=30')&&scheduler.includes('reactionDelayTimer=setTimeout')&&scheduler.includes('requestAnimationFrame')&&scheduler.includes('reactionNextDrawAt'),'Reaction rendering is not deadline-scheduled at the existing 30 FPS cadence');
|
|
assert(scheduler.includes("document.visibilityState==='hidden'")&&functionSource('cancelReactionRenderScheduler').includes('reactionLastDraw=0'),'Reaction scheduling is not suspended and reset across hidden-page lifecycle changes');
|
|
|
|
const prepare=functionSource('prepareReactionModel');
|
|
for(const token of ['preparedReactionCracks','preparedLaserRays','preparedOrbitModel','preparedFireworkBloom','preparedReactionBurst'])assert(prepare.includes(token)||app.includes(token),`Prepared reaction model is missing ${token}`);
|
|
assert(functionSource('normalizeRealtimeReaction').includes('reaction.prepared=prepareReactionModel(reaction)')&&functionSource('drawStyledReaction').includes('reaction.prepared'),'Immutable reaction work is not prepared once and reused');
|
|
|
|
assert(app.includes('REACTION_GLYPH_CACHE_MAX_ENTRIES=160')&&app.includes('REACTION_GLYPH_CACHE_MAX_BYTES=12*1024*1024'),'Reaction glyph cache is not explicitly bounded');
|
|
assert(app.includes('REACTION_STATIC_PATH_CACHE_MAX_ENTRIES=32')&&functionSource('reactionStaticPath').includes('reactionStaticPathCacheEvictions'),'Reaction static Path2D cache is not bounded or measured');
|
|
assert(functionSource('drawOrbitReaction').includes('reactionOrbitRingPath')&&functionSource('drawFireworkReaction').includes('reactionFireworkLaunchPath'),'Static reaction geometry does not use the exact Path2D cache');
|
|
const glyph=functionSource('reactionGlyphSprite');
|
|
assert(glyph.includes('reactionGlyphCacheHits')&&glyph.includes('reactionGlyphCacheMisses')&&glyph.includes('reactionGlyphCacheEvictions'),'Reaction glyph cache metrics or eviction are missing');
|
|
assert(functionSource('drawReactionEmoji').includes('drawImage')&&functionSource('drawReactionEmoji').includes('fillText'),'Cached glyph drawing lacks its exact direct-render fallback');
|
|
|
|
const reactionLayer=functionSource('drawReactionLayer');
|
|
for(const metric of ['reactionFrame','reactionComposite','activeReactions','visibleReactions','peakVisibleReactions','preparedReactionModels','reactionOverload'])assert(app.includes(metric),`Effect performance metric is missing: ${metric}`);
|
|
for(const style of ['giant','laser','orbit','firework','comet'])assert(functionSource('drawStyledReaction').includes(`style==='${style}'`),`Full reaction renderer is missing: ${style}`);
|
|
assert(!functionSource('drawStyledReaction').match(/interactionActive|lightweightRendering|autoReduced|fallbackStyle|visibleReactionLimit/),'Reaction renderer contains automatic or interaction-driven visual degradation');
|
|
assert(reactionLayer.includes('for(const[id,reaction]of realtimeReactions)')&&!reactionLayer.match(/slice\(|break;|visibleReactionLimit/),'Valid overlapping reactions can be dropped from rendering');
|
|
|
|
const aurora=functionSource('startAuroraRgbAnimation'),auroraState=functionSource('updateAuroraAnimationState');
|
|
assert(aurora.includes('AURORA_COLOR_INTERVAL')&&auroraState.includes('auroraVisiblePathCount')&&!aurora.includes('querySelector'),'Aurora is not controlled by tracked visible paths');
|
|
assert(functionSource('writeAuroraRgb').includes('auroraColorHost().style.setProperty')&&!functionSource('writeAuroraRgb').includes('document.body.style'),'Aurora color invalidation is not scoped to the world');
|
|
assert(functionSource('useInventoryItemLoaded').slice(functionSource('useInventoryItemLoaded').indexOf('if(item.lineColor)'),functionSource('useInventoryItemLoaded').indexOf('if(item.lineEffect)')).includes('renderAll()'),'Equipping a line color does not immediately repaint lines and gates');
|
|
|
|
assert(app.includes('GEM_PARTICLE_POOL_LIMIT=72')&&functionSource('playGemCollectionAnimation').includes('document.createDocumentFragment()')&&functionSource('releaseGemParticle').includes('gemParticlePool.push'),'Gem animations do not use a bounded, batched node pool');
|
|
assert(app.includes('GEM_PARTICLE_KEYFRAME_TEMPLATE=Object.freeze([')&&app.includes('GEM_PARTICLE_ANIMATION_OPTIONS_TEMPLATE=Object.freeze(')&&functionSource('takeGemParticle').includes('if(!particle._gemKeyframes)')&&functionSource('animateGemParticle').includes('particle.animate(keyframes,options)'),'Gem animation templates are still allocated for every playback or shared unsafely across concurrent particles');
|
|
assert(app.includes('COMPLETION_NODE_POOL_LIMIT=16')&&functionSource('finishCompletionVisual').includes('releaseCompletionNode'),'Completion visuals do not return their nodes to a bounded pool');
|
|
|
|
const inventory=functionSource('renderInventoryPanel');
|
|
assert(app.includes('inventoryCategoryViews=new Map()')&&app.includes('inventoryItemViews=new Map()')&&!inventory.includes('inventoryList.replaceChildren()'),'Owned cosmetic rendering still rebuilds the full list');
|
|
assert(functionSource('setItemIcon').includes("image.loading='lazy'")&&functionSource('setItemIcon').includes("image.decoding='async'"),'Flag assets are not lazy-loaded and asynchronously decoded');
|
|
assert(css.includes('content-visibility:auto')&&css.includes('contain-intrinsic-size'),'Offscreen cosmetic cards do not use render containment');
|
|
assert(functionSource('syncInventoryCursorSelection').includes('inventorySelectedCursorItemId')&&!functionSource('syncInventoryCursorSelection').includes('querySelectorAll'),'Cursor equip still scans the entire catalog');
|
|
assert(functionSource('renderInventoryPanel').includes('inventoryItemsUnchanged')||functionSource('updateInventoryItemView').includes('inventoryItemsUnchanged'),'Inventory reconciliation does not skip unchanged cards');
|
|
const inventoryUse=functionSource('useInventoryItemLoaded');
|
|
for(const marker of ['if(item.lineColor)','if(item.reactionStyle)','if(item.scoreLens)'])assert(inventoryUse.slice(inventoryUse.indexOf(marker)).includes('patchInventoryItems'),'Cosmetic equip is not locally patched');
|
|
|
|
assert(app.includes('renderReactionSample:options=>renderReactionSample(options)')&&functionSource('renderReactionSample').includes('normalizedLife'),'Exact-lifetime visual-equivalence test hook is missing');
|
|
|
|
for(const gate of ["timing(result.setup.reaction,'reactionPublish').p95<=2","timing(result.setup.gem,'gemEffectSetup').p95<=3","timing(result.setup.completion,'completionEffectSetup').p95<=3","patch.p95<=8","focusRetained","auroraColorWrites","gemBatches===0","effectNodes===0"])assert(browserBenchmark.includes(gate),`Browser release gate is missing: ${gate}`);
|
|
for(const interaction of ['benchmark-pickup-effect','benchmark-edge-pan-effect','benchmark-overview-effect'])assert(browserBenchmark.includes(interaction),`Effect interaction probe is missing: ${interaction}`);
|
|
|
|
assert(!app.includes('autoReducedEffects')&&!app.includes('reactionQualityTier')&&!app.includes('dropReactionForPerformance'),'Automatic effect quality degradation was introduced');
|
|
|
|
console.log('Effects and cosmetics performance architecture smoke test passed');
|