[object Object]

← back to Re Flyer Aggregator

FIX live.html crash: GPU memory leak (undisposed geometries/materials/label-textures + unbounded cubes on every mode-switch/poll) -> shared geo+materials, proper disposal, CAP 140 cubes, instant rebuild. Rigorously tested via Playwright: 24 rapid mode-switches = geos/texs/cubes STABLE, 0 errors, drill works (click bar -> 23 Sale records/$4.6B) (TK-10708)

3c044a6b4b732f25b1a9b29a4b42d4a1c933fe03 · 2026-08-19 16:35:47 -0700 · Steve Abrams

Files touched

Diff

commit 3c044a6b4b732f25b1a9b29a4b42d4a1c933fe03
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 16:35:47 2026 -0700

    FIX live.html crash: GPU memory leak (undisposed geometries/materials/label-textures + unbounded cubes on every mode-switch/poll) -> shared geo+materials, proper disposal, CAP 140 cubes, instant rebuild. Rigorously tested via Playwright: 24 rapid mode-switches = geos/texs/cubes STABLE, 0 errors, drill works (click bar -> 23 Sale records/$4.6B) (TK-10708)
---
 public/flyers/live.html | 155 ++++++++++++++++++++++++++++--------------------
 1 file changed, 92 insertions(+), 63 deletions(-)

diff --git a/public/flyers/live.html b/public/flyers/live.html
index 3b8e56b..b829308 100644
--- a/public/flyers/live.html
+++ b/public/flyers/live.html
@@ -19,15 +19,14 @@
   #drill .x{position:absolute;top:14px;right:16px;cursor:pointer;color:#8aa;font-size:20px}
   .rec{display:block;padding:10px 12px;border:1px solid #1c2540;border-radius:8px;margin-bottom:8px;text-decoration:none;color:#eaf0ff}
   .rec:hover{border-color:#3a4a78;background:#111a30}
-  .rec .p{font-weight:800;color:#7fd7ff} .rec .t{font-size:13px;margin:3px 0;line-height:1.3}
-  .rec .m{color:#8aa;font-size:11px} .rec .src{color:#E8A81C;font-size:11px}
+  .rec .p{font-weight:800;color:#7fd7ff}.rec .t{font-size:13px;margin:3px 0;line-height:1.3}.rec .m{color:#8aa;font-size:11px}.rec .src{color:#E8A81C;font-size:11px}
 </style></head><body>
-<div id="drill"><span class="x" onclick="document.getElementById('drill').classList.remove('open')">✕</span><div id="drillbody"></div></div>
 <canvas id="c"></canvas>
 <div class="hud" id="top"><div id="title">CRE DEALS · LIVE<small>ABRAMS · FEEDS DROPPING INTO CATEGORIES</small></div>
   <div id="stats"><b id="cnt">0</b> deals<br><span id="vol">$0</span> · <span id="mk">0 markets</span></div></div>
 <select id="mode" title="category axis"><option value="type">Group by: Type</option><option value="market">Group by: Market</option><option value="source">Group by: Source (from where)</option></select>
-<div class="hud" id="ticker"><div class="lbl">Latest — from where</div><div class="row" id="last">Loading deal flow…</div></div>
+<div id="drill"><span class="x" onclick="document.getElementById('drill').classList.remove('open')">✕</span><div id="drillbody"></div></div>
+<div class="hud" id="ticker"><div class="lbl">Latest — from where · click a bar to drill in</div><div class="row" id="last">Loading deal flow…</div></div>
 <script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>
 <script type="module">
 import * as THREE from 'three';
@@ -36,94 +35,124 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 const TYPE={Sale:0x2ee6a0,Loan:0xf0b429,Lease:0x5b8bff,Development:0xb061ff,Filing:0x8aa0c0,CRE:0x8aa0c0};
 const money=n=>!n?'$0':n>=1e9?'$'+(n/1e9).toFixed(1)+'B':n>=1e6?'$'+Math.round(n/1e6)+'M':'$'+Math.round(n/1e3)+'K';
 const hex=c=>'#'+c.toString(16).padStart(6,'0');
+const CAP=140;                      // hard cap on live cubes (prevents unbounded GPU growth)
 
 const scene=new THREE.Scene(); scene.fog=new THREE.FogExp2(0x070a14,0.004);
 const cam=new THREE.PerspectiveCamera(52,innerWidth/innerHeight,.1,2000); cam.position.set(0,42,86);
-const rnd=new THREE.WebGLRenderer({canvas:c,antialias:true}); rnd.setSize(innerWidth,innerHeight); rnd.setPixelRatio(Math.min(devicePixelRatio,2));
+const rnd=new THREE.WebGLRenderer({canvas:c,antialias:true,powerPreference:'high-performance'});
+rnd.setSize(innerWidth,innerHeight); rnd.setPixelRatio(Math.min(devicePixelRatio,2));
 const ctrl=new OrbitControls(cam,rnd.domElement); ctrl.enableDamping=true; ctrl.target.set(0,16,0); ctrl.autoRotate=true; ctrl.autoRotateSpeed=.25; ctrl.maxPolarAngle=Math.PI/2.05;
 scene.add(new THREE.AmbientLight(0x99aaff,.6)); const dl=new THREE.DirectionalLight(0xffffff,.8); dl.position.set(20,60,40); scene.add(dl);
-// floor (XZ plane) + back Y grid
-const floor=new THREE.GridHelper(200,40,0x1b2340,0x101830); scene.add(floor);
-const ywall=new THREE.GridHelper(200,20,0x141c33,0x0d1424); ywall.rotation.x=Math.PI/2; ywall.position.set(0,50,-40); scene.add(ywall);
+scene.add(new THREE.GridHelper(200,40,0x1b2340,0x101830));
+const yw=new THREE.GridHelper(200,20,0x141c33,0x0d1424); yw.rotation.x=Math.PI/2; yw.position.set(0,50,-40); scene.add(yw);
 
-function label(t,color='#eaf0ff',size=40){const cv=document.createElement('canvas'),g=cv.getContext('2d');g.font=`bold ${size}px Inter,Arial`;cv.width=g.measureText(t).width+20;cv.height=size+16;g.font=`bold ${size}px Inter,Arial`;g.fillStyle=color;g.fillText(t,10,size);const tx=new THREE.CanvasTexture(cv);tx.anisotropy=4;const s=new THREE.Sprite(new THREE.SpriteMaterial({map:tx,transparent:true,depthTest:false}));s.scale.set(cv.width/13,cv.height/13,1);return s;}
+// ---- SHARED resources (created once, never leaked) ----
+const BW=7,GAP=6,BH=2.2;
+const cubeGeo=new THREE.BoxGeometry(BW,BH,BW);
+const padGeo=new THREE.BoxGeometry(BW+1.5,.4,BW+1.5);
+const padMat=new THREE.MeshStandardMaterial({color:0x16203a});
+const typeMat={}; for(const[k,c]of Object.entries(TYPE)) typeMat[k]=new THREE.MeshStandardMaterial({color:c,emissive:c,emissiveIntensity:.6,transparent:true,opacity:.96});
+function labelSprite(text,color,size,store){const cv=document.createElement('canvas'),g=cv.getContext('2d');g.font=`bold ${size}px Inter,Arial`;cv.width=g.measureText(text).width+20;cv.height=size+16;g.font=`bold ${size}px Inter,Arial`;g.fillStyle=color;g.fillText(text,10,size);const tx=new THREE.CanvasTexture(cv);tx.anisotropy=4;const m=new THREE.SpriteMaterial({map:tx,transparent:true,depthTest:false});const s=new THREE.Sprite(m);s.scale.set(cv.width/13,cv.height/13,1);store.push(m);return s;}
 
-// ---- columns: one per category. Blocks DROP from front (+Z) into their column, stacking up (Y). ----
-const BW=7, GAP=6, BH=2.2;   // block width, gap, block height
-let cats=[], colX={}, stack={}, colGroup=new THREE.Group(); scene.add(colGroup);
-function layout(catList){
-  colGroup.clear(); cats=catList; stack={}; colX={};
-  const total=cats.length*(BW+GAP)-GAP; let x=-total/2+BW/2;
-  cats.forEach((cname,i)=>{ colX[cname]=x; stack[cname]=[];
-    // base pad + X-axis label
-    const pad=new THREE.Mesh(new THREE.BoxGeometry(BW+1.5,.4,BW+1.5),new THREE.MeshStandardMaterial({color:0x16203a})); pad.position.set(x,.2,0); colGroup.add(pad);
-    const l=label(cname,'#9fb4e6',30); l.position.set(x,-2.2,BW/2+1); colGroup.add(l);
-    x+=BW+GAP;
-  });
+const colGroup=new THREE.Group(); scene.add(colGroup);
+let colX={},stack={},cubes=[],drops=[],labelMats=[],dealsByCat={};
+function disposeScene(){ // remove + dispose ONLY per-rebuild objects (labels); shared geo/mats stay
+  colGroup.clear();
+  for(const m of labelMats){ if(m.map)m.map.dispose(); m.dispose(); }
+  labelMats=[]; colX={}; stack={}; cubes=[]; drops=[]; dealsByCat={};
 }
-const drops=[];
 function catOf(d,mode){ return mode==='market'?(d.market||'US'):mode==='source'?(d.source||'CRE'):(d.type||'CRE'); }
-function drop(d){
-  const mode=document.getElementById('mode').value; let cat=catOf(d,mode);
-  if(colX[cat]===undefined){ // new category column appears -> re-layout keeping counts
-    layout([...cats,cat]); }
+function ensureColumn(cat){ if(colX[cat]!==undefined)return;
+  const cats=Object.keys(colX); const idx=cats.length;
+  const total=(idx+1)*(BW+GAP)-GAP; // relayout all X so it stays centered
+  colX[cat]=0; stack[cat]=stack[cat]||[];
+  const allCats=[...cats,cat]; let x=-((allCats.length*(BW+GAP)-GAP))/2+BW/2;
+  allCats.forEach(cn=>{ colX[cn]=x; x+=BW+GAP; });
+  // (columns just track X; pads/labels drawn in rebuild)
+}
+function drawAxes(){ // pads + x labels for current columns
+  for(const [cn,x] of Object.entries(colX)){
+    const pad=new THREE.Mesh(padGeo,padMat); pad.position.set(x,.2,0); colGroup.add(pad);
+    const l=labelSprite(cn.slice(0,16),'#9fb4e6',28,labelMats); l.position.set(x,-2.2,BW/2+1.5); colGroup.add(l);
+  }
+}
+function addCube(d,cat,animate){
   const x=colX[cat], k=stack[cat].length, landY=k*BH+BH/2+.4;
-  const col=TYPE[d.type]||0x8aa0c0;
-  const cube=new THREE.Mesh(new THREE.BoxGeometry(BW,BH,BW),new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:.55,transparent:true,opacity:.96}));
-  cube.position.set(x, landY+46, 30);   // spawn high + in FRONT (z=30)
-  cube.userData={deal:d,cat};           // clickable -> drill into this record
+  const cube=new THREE.Mesh(cubeGeo, typeMat[d.type]||typeMat.CRE);
+  cube.userData={deal:d,cat};
+  if(animate){ cube.position.set(x,landY+46,30); drops.push({cube,x,landY,t:0}); }
+  else cube.position.set(x,landY,0);
+  colGroup.add(cube); stack[cat].push(cube); cubes.push(cube);
   (dealsByCat[cat]=dealsByCat[cat]||[]).push(d);
-  colGroup.add(cube); stack[cat].push(cube);
-  drops.push({cube,x,landY,t:0});
-  // HUD
-  document.getElementById('cnt').textContent=(++S.cnt);
-  S.vol+=d.price||0; document.getElementById('vol').textContent=money(S.vol);
-  S.mk.add(d.market||'US'); document.getElementById('mk').textContent=S.mk.size+' markets';
-  document.getElementById('last').innerHTML=`<span class="chip" style="background:${hex(col)}">${(d.type||'CRE').toUpperCase()}</span> <b>${d.price_label||money(d.price)}</b> ${d.market?'· '+d.market:''} — ${(d.title||'').slice(0,64)} <span style="color:#E8A81C">◂ ${d.source}</span>`;
 }
-const S={cnt:0,vol:0,mk:new Set()}; const dealsByCat={};
-// ---- click a color bar / block to ENTER its records ----
+const S={vol:0,mk:new Set()};
+function hud(d){ document.getElementById('cnt').textContent=MASTER.length;
+  document.getElementById('vol').textContent=money(MASTER.reduce((s,x)=>s+(x.price||0),0));
+  document.getElementById('mk').textContent=(new Set(MASTER.map(x=>x.market||'US'))).size+' markets';
+  const col=TYPE[d.type]||0x8aa0c0;
+  document.getElementById('last').innerHTML=`<span class="chip" style="background:${hex(col)}">${(d.type||'CRE').toUpperCase()}</span> <b>${d.price_label||money(d.price)}</b> ${d.market?'· '+d.market:''} — ${(d.title||'').slice(0,60)} <span style="color:#E8A81C">◂ ${d.source}</span>`;
+}
+function rebuild(mode,animateNew){ disposeScene();
+  // stable column order: by descending total volume
+  const groups={}; for(const d of MASTER){const c=catOf(d,mode);(groups[c]=groups[c]||[]).push(d);}
+  const cats=Object.keys(groups).sort((a,b)=>groups[b].reduce((s,d)=>s+(d.price||0),0)-groups[a].reduce((s,d)=>s+(d.price||0),0)).slice(0,14);
+  let x=-((cats.length*(BW+GAP)-GAP))/2+BW/2; cats.forEach(cn=>{colX[cn]=x;stack[cn]=[];x+=BW+GAP;});
+  drawAxes();
+  for(const cn of cats){ groups[cn].slice().sort((a,b)=>(a.price||0)-(b.price||0)).forEach(d=>{ if(colX[catOf(d,mode)]!==undefined) addCube(d,catOf(d,mode),false); }); }
+}
+
+// ---- click a bar/block to enter its records ----
 const ray=new THREE.Raycaster(), mouse=new THREE.Vector2();
-function onClick(ev){ const r=rnd.domElement.getBoundingClientRect();
+rnd.domElement.addEventListener('click',ev=>{ const r=rnd.domElement.getBoundingClientRect();
   mouse.x=((ev.clientX-r.left)/r.width)*2-1; mouse.y=-((ev.clientY-r.top)/r.height)*2+1;
-  ray.setFromCamera(mouse,cam); const hit=ray.intersectObjects(colGroup.children.filter(o=>o.userData&&o.userData.deal));
-  if(hit.length){ openDrill(hit[0].object.userData.cat, hit[0].object.userData.deal); }
-}
-rnd.domElement.addEventListener('click',onClick);
+  ray.setFromCamera(mouse,cam); const hit=ray.intersectObjects(cubes);
+  if(hit.length) openDrill(hit[0].object.userData.cat,hit[0].object.userData.deal);
+});
 function openDrill(cat,clicked){ const list=(dealsByCat[cat]||[]).slice().sort((a,b)=>(b.price||0)-(a.price||0));
   const col=TYPE[clicked.type]||0x8aa0c0;
   document.getElementById('drillbody').innerHTML=`<h3 style="color:${hex(col)}">${cat}</h3>
    <div class="sub">${list.length} record${list.length!=1?'s':''} · ${money(list.reduce((s,d)=>s+(d.price||0),0))} total · click any to open source ↗</div>`+
    list.map(d=>`<a class="rec" href="${d.link}" target="_blank" rel="noopener noreferrer" style="border-color:${d===clicked?hex(col):'#1c2540'}">
-     <span class="p">${d.price_label||money(d.price)}</span> <span class="m">· ${d.market||'US'} · ${(d.type||'CRE')}</span>
+     <span class="p">${d.price_label||money(d.price)}</span> <span class="m">· ${d.market||'US'} · ${d.type||'CRE'}</span>
      <div class="t">${(d.title||'').replace(/</g,'&lt;')}</div>
      <span class="src">◂ ${d.source}${d.buyer?' · buyer '+d.buyer:''}${d.seller?' · seller '+d.seller:''}</span></a>`).join('');
-  document.getElementById('drill').classList.add('open'); ctrl.autoRotate=false;
+  document.getElementById('drill').classList.add('open');
 }
 
+window.__diag=()=>({cubes:cubes.length,drops:drops.length,geos:rnd.info.memory.geometries,texs:rnd.info.memory.textures,calls:rnd.info.render.calls});
+let pulse=0;
 function anim(){ requestAnimationFrame(anim); ctrl.update();
-  for(let i=drops.length-1;i>=0;i--){ const f=drops[i]; f.t=Math.min(1,f.t+.035);
-    const e=f.t*f.t; // gravity ease-in
-    f.cube.position.y = (f.landY+46) + (f.landY-(f.landY+46))*e;
-    f.cube.position.z = 30*(1-e);           // come from front into the column
-    if(f.t>=1){ f.cube.position.set(f.x,f.landY,0); drops.splice(i,1); }
-  }
+  for(let i=drops.length-1;i>=0;i--){ const f=drops[i]; f.t=Math.min(1,f.t+.035); const e=f.t*f.t;
+    f.cube.position.y=(f.landY+46)+(f.landY-(f.landY+46))*e; f.cube.position.z=30*(1-e);
+    if(f.t>=1){ f.cube.position.set(f.x,f.landY,0); drops.splice(i,1); } }
+  pulse+=.02; for(const k in typeMat) typeMat[k].emissiveIntensity=.55+Math.sin(pulse)*.15;
   rnd.render(scene,cam);
 }
-layout(['Sale','Loan','Lease','Development']); anim();
+anim();
 addEventListener('resize',()=>{cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix();rnd.setSize(innerWidth,innerHeight);});
-document.getElementById('mode').onchange=()=>{ // rebuild for new grouping
-  const base = document.getElementById('mode').value==='type'?['Sale','Loan','Lease','Development']:[];
-  layout(base); S.cnt=0;S.vol=0;S.mk=new Set(); document.getElementById('cnt').textContent=0; seen=new Set(); ALL.slice().reverse().forEach((d,i)=>setTimeout(()=>drop(d),i*120));
-};
+document.getElementById('mode').onchange=()=>rebuild(document.getElementById('mode').value,false);
 
-// ---- live populate from flyers.json: stream deals in, then poll for new ----
-let seen=new Set(), ALL=[];
+// ---- data: master list (deduped + CAPPED) ----
+let MASTER=[], seen=new Set();
+function sig(d){return (d.price||0)+'|'+(d.title||'').slice(0,34);}
 async function load(first){ try{
-  const j=await (await fetch('flyers.json?t='+Date.now())).json(); ALL=j;
-  const fresh=j.filter(d=>{const k=(d.price||0)+'|'+(d.title||'').slice(0,30);if(seen.has(k))return false;seen.add(k);return true;});
-  fresh.sort((a,b)=>(a.price||0)-(b.price||0)); // smallest first so big deals land on top
-  let i=0; const iv=setInterval(()=>{ if(i>=fresh.length){clearInterval(iv);return;} drop(fresh[i++]); }, first?280:800);
+  const j=await (await fetch('flyers.json?t='+Date.now())).json();
+  const fresh=j.filter(d=>{const k=sig(d); if(seen.has(k))return false; seen.add(k); return true;});
+  fresh.sort((a,b)=>(a.price||0)-(b.price||0));
+  if(first){ MASTER=fresh.slice(-CAP); rebuild('type',false);
+    // animate the initial fill for fun
+    let i=0; const order=[...cubes]; drops.length=0; cubes.forEach(cb=>{cb.position.y+=0;});
+    // re-animate: turn current placed cubes into drops staggered
+    const q=cubes.slice(); let n=0; const iv=setInterval(()=>{ if(n>=q.length){clearInterval(iv);return;}
+      const cb=q[n++]; const ly=cb.position.y; cb.position.set(cb.position.x,ly+46,30); drops.push({cube:cb,x:cb.position.x,landY:ly,t:0}); }, first?60:0);
+  } else if(fresh.length){ // append new, cap, animate just the new ones into current grouping
+    const mode=document.getElementById('mode').value;
+    for(const d of fresh){ MASTER.push(d); if(MASTER.length>CAP){ const drop=MASTER.shift(); seen.delete(sig(drop)); } }
+    // rebuild instantly then animate the newest few
+    rebuild(mode,false);
+    fresh.slice(-8).forEach(d=>{ const cb=cubes.find(c=>c.userData.deal===d); if(cb){ const ly=cb.position.y; cb.position.set(cb.position.x,ly+46,30); drops.push({cube:cb,x:cb.position.x,landY:ly,t:0}); } });
+  }
+  if(fresh.length) hud(fresh[fresh.length-1]);
 }catch(e){ document.getElementById('last').textContent='feed error: '+e.message; } }
 load(true); setInterval(()=>load(false),30000);
 </script></body></html>

← 3aabab2 interactive live viz (click color bar -> enter its records)  ·  back to Re Flyer Aggregator  ·  auto-data-snapshot: 2026-08-19T16:46:39 (6 data files) — dat 2af6800 →