Skip to content

Move link range and packed link texture generation into WASM#36

Merged
jonobr1 merged 4 commits intomainfrom
codex/wasm-link-range-packing
Feb 21, 2026
Merged

Move link range and packed link texture generation into WASM#36
jonobr1 merged 4 commits intomainfrom
codex/wasm-link-range-packing

Conversation

@jonobr1
Copy link
Copy Markdown
Owner

@jonobr1 jonobr1 commented Feb 14, 2026

Summary

  • generate packed incident links and per-node link ranges in AssemblyScript WASM
  • return linkRanges and packedLinkAmount from worker processing (WASM + JS fallback)
  • consume worker-provided links/linkRanges in graph setup; keep CPU packing as fallback
  • update built wasm and distributed build artifacts

Files

  • src/wasm/texture-processor.ts
  • src/workers/texture-worker.js
  • src/workers/texture-worker-simple.js
  • src/inline-worker-factory.js
  • src/index.js
  • build/texture-processor.wasm (+ map/wat)
  • build/fdg.js
  • build/fdg.module.js
  • build/fdg.umd.js

Copilot AI review requested due to automatic review settings February 14, 2026 00:06
@claude
Copy link
Copy Markdown

claude bot commented Feb 14, 2026

Claude finished @jonobr1's task —— View job


Code Review Completed

I've reviewed this PR that moves link range and packed link texture generation into WASM. Here's my comprehensive feedback:

🟢 Strengths

WASM Implementation Quality:

  • Well-structured AssemblyScript code in texture-processor.ts:21-196 with clear function signatures and documentation
  • Proper memory management with explicit allocation/deallocation patterns (texture-processor.ts:96-98, 191-194)
  • Good separation of concerns between node position processing and link packing

Robust Worker Architecture:

  • Excellent fallback strategy: WASM → JavaScript worker → main thread processing
  • Multiple worker implementations for different environments (module vs simple)
  • Proper error handling and status reporting throughout the chain

Performance Optimizations:

  • Uses transferable objects for efficient data transfer (texture-worker.js:216, 286)
  • Memory-efficient processing with streaming data copies
  • Performance timing and memory usage monitoring included

🟡 Areas for Improvement

Memory Safety Concerns

Critical Issue in texture-processor-simple.js:135-148:

// Simple memory layout - allocate sequentially  
let memoryOffset = 0;
const nodesDataPtr = memoryOffset;
memoryOffset += nodesDataSize;
// ...

This manual memory layout is dangerous - it doesn't account for alignment requirements and could cause crashes on certain platforms. The main worker correctly uses allocateMemory().

Recommendation: Use the proper WASM allocation functions consistently across all worker variants.

Input Validation

Missing bounds checking in multiple locations:

  • texture-processor.ts:108-111: Link validation only checks basic bounds, not buffer overflow potential
  • texture-worker.js:154-160: No validation that input arrays match expected sizes
  • Workers trust input data length without verification

Recommendation: Add comprehensive input validation before WASM calls:

if (nodes.length * 4 * 4 > MAX_SAFE_SIZE) {
  throw new Error(`Input too large: ${nodes.length} nodes`);
}

Error Handling Gaps

WASM error recovery in texture-worker.js:218-225:
The error handling doesn't distinguish between recoverable (memory) and non-recoverable (logic) errors. All failures just return a generic error message.

Resource cleanup: If WASM processing fails partway through, allocated memory may leak.

Performance Considerations

Memory allocation pattern in texture-processor.ts:95-99:

const degreeCountsPtr = heap.alloc(intsSize);
const startOffsetsPtr = heap.alloc(intsSize);  
const cursorsPtr = heap.alloc(intsSize);

Multiple small allocations vs. one large allocation + offsetting would be more cache-efficient.

Data copying overhead: Node/link data is copied multiple times through the worker pipeline. Consider streaming or shared array buffers for large datasets.

🔒 Security Assessment

Generally secure implementation:

  • No direct memory access from JavaScript
  • Proper bounds checking in WASM for most operations
  • Worker sandboxing maintains security boundaries

Minor concern: The inline worker factory creates workers from blob URLs, which could be flagged by CSP policies in strict environments.

🧪 Test Coverage

Missing test scenarios:

  • Edge cases like empty graphs, single nodes, self-loops
  • Memory exhaustion scenarios
  • WASM initialization failures
  • Worker termination during processing

Performance regression tests would be valuable given this is an optimization PR.

📊 Architecture Assessment

Well-designed fallback chain that gracefully degrades:

  1. WASM worker processing (fastest)
  2. JavaScript worker processing (medium)
  3. Main thread processing (fallback)

Good separation between worker management (texture-worker-manager.js) and processing logic.

🏁 Conclusion

This is a solid performance enhancement with a well-thought-out architecture. The main concerns are around memory safety in the simple worker implementation and input validation. I'd recommend:

  1. Fix the memory allocation pattern in texture-worker-simple.js
  2. Add comprehensive input validation before WASM calls
  3. Enhance error specificity to help with debugging
  4. Consider adding integration tests for the worker pipeline

The performance benefits and graceful degradation make this a valuable addition to the codebase.

Overall Assessment: ✅ Approve with recommended improvements

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves incident-link packing (packed link texture + per-node link ranges) into the AssemblyScript WASM texture processor and threads the new outputs (linkRanges, packedLinkAmount) through the worker pipeline into graph setup/shaders, with JS/main-thread fallbacks retained.

Changes:

  • Extend WASM processTextures/processLinks to generate packed link UVs + linkRanges, returning packedLinkAmount.
  • Update worker(s) and graph setup to consume worker-provided links/linkRanges (and fall back to JS packing when needed).
  • Update bundled/dist artifacts (WASM + built fdg outputs) to use the new textureLinkRanges shader path.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/wasm/texture-processor.ts Adds packed link + link-range generation in WASM and returns packedLinkAmount.
src/workers/texture-worker.js WASM worker now allocates/extracts/sends linkRanges and packedLinkAmount; JS fallback packs incident links.
src/workers/texture-worker-simple.js Non-module worker updated similarly (but currently has correctness issues in WASM path).
src/inline-worker-factory.js Inline worker code updated to send linkRanges/packedLinkAmount and includes JS fallback packing.
src/index.js Consumes worker-provided links/linkRanges and sets edgeAmount to packed link count; main-thread packing kept as fallback.
build/texture-processor.wasm Updated compiled WASM binary.
build/texture-processor.wat Updated WAT reflecting new exports/signatures and packed-link implementation.
build/fdg.js Bundled build updated: new link-range shader usage and updated worker/graph logic.
build/fdg.module.js ESM bundle updated accordingly.
build/fdg.umd.js UMD bundle updated accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 223 to 229
data: {
positions: positionsResult,
links: linksResult,
linkRanges: linkRangesResult,
packedLinkAmount,
processingTime,
memoryUsage: memory.buffer.byteLength
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packedLinkAmount is declared inside the if (wasmModule.exports.processTextures) block but is referenced later in the postMessage payload. This will throw a ReferenceError and break worker processing. Define packedLinkAmount in the outer scope (e.g., let packedLinkAmount = 0;) and assign it from the WASM call before using it in the response.

Copilot uses AI. Check for mistakes.
Comment thread src/inline-worker-factory.js Outdated
Comment on lines 178 to 189
const linksTexturePtr = memoryOffset;
memoryOffset += linksTextureSize;
const linkRangesTexturePtr = memoryOffset;

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline worker uses memoryOffset = 0 for all input/output buffers. Since the WASM implementation now allocates temporary arrays via heap.alloc inside processLinks, this can cause heap allocations to overlap with these buffers and corrupt memory. Switch this path to use the exported allocateMemory/freeMemory API (or an allocation strategy that reserves space above the heap base and avoids overlap with the WASM heap).

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +193
if (packedLinkAmount > totalElements) {
heap.free(degreeCountsPtr);
heap.free(startOffsetsPtr);
heap.free(cursorsPtr);
return -1;
}

for (let i = 0; i < linksCount; i++) {
const linkDataOffset = linksDataPtr + i * 2 * FLOAT32_BYTES;
const sourceIndex = load<i32>(linkDataOffset + 0 * FLOAT32_BYTES);
const targetIndex = load<i32>(linkDataOffset + 1 * FLOAT32_BYTES);

const isValid =
sourceIndex >= 0 &&
sourceIndex < nodesCount &&
targetIndex >= 0 &&
targetIndex < nodesCount;

if (!isValid) {
continue;
}

const sourceOffset = sourceIndex * FLOAT32_BYTES;
let sourceCursor = load<i32>(cursorsPtr + sourceOffset);
store<i32>(cursorsPtr + sourceOffset, sourceCursor + 1);

let linkOffset = linksTexturePtr + sourceCursor * texelStride;
store<f32>(linkOffset + 0 * FLOAT32_BYTES, f32(sourceIndex % textureSize) / textureSizeF);
store<f32>(linkOffset + 1 * FLOAT32_BYTES, f32(sourceIndex / textureSize) / textureSizeF);
store<f32>(linkOffset + 2 * FLOAT32_BYTES, f32(targetIndex % textureSize) / textureSizeF);
store<f32>(linkOffset + 3 * FLOAT32_BYTES, f32(targetIndex / textureSize) / textureSizeF);

if (sourceIndex != targetIndex) {
const targetOffset = targetIndex * FLOAT32_BYTES;
let targetCursor = load<i32>(cursorsPtr + targetOffset);
store<i32>(cursorsPtr + targetOffset, targetCursor + 1);

linkOffset = linksTexturePtr + targetCursor * texelStride;
store<f32>(linkOffset + 0 * FLOAT32_BYTES, f32(sourceIndex % textureSize) / textureSizeF);
store<f32>(linkOffset + 1 * FLOAT32_BYTES, f32(sourceIndex / textureSize) / textureSizeF);
store<f32>(linkOffset + 2 * FLOAT32_BYTES, f32(targetIndex % textureSize) / textureSizeF);
store<f32>(linkOffset + 3 * FLOAT32_BYTES, f32(targetIndex / textureSize) / textureSizeF);
}
}

heap.free(degreeCountsPtr);
heap.free(startOffsetsPtr);
heap.free(cursorsPtr);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heap.free in the stub runtime only reclaims memory when freeing the most-recent allocation (LIFO). Here, pointers are freed in allocation order (degreeCounts, startOffsets, cursors), so most allocations won’t be reclaimed and memory will grow over time. Free in reverse allocation order (free cursorsPtr first, then startOffsetsPtr, then degreeCountsPtr) in both the overflow early-return path and the normal return path.

Copilot uses AI. Check for mistakes.
Comment thread src/workers/texture-worker.js Outdated
Comment on lines +194 to +199
// Free WASM memory
wasmModule.exports.freeMemory(nodesDataPtr);
wasmModule.exports.freeMemory(linksDataPtr);
wasmModule.exports.freeMemory(positionsPtr);
wasmModule.exports.freeMemory(linksTexturePtr);
wasmModule.exports.freeMemory(linkRangesTexturePtr);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WASM allocations are freed in the same order they’re allocated. With the current AssemblyScript stub __free behavior (LIFO-only), this won’t actually reclaim memory and repeated processing can steadily grow memory usage. Free pointers in reverse allocation order (e.g., linkRangesTexturePtr, linksTexturePtr, positionsPtr, linksDataPtr, nodesDataPtr) or allocate a single contiguous block and free it once.

Suggested change
// Free WASM memory
wasmModule.exports.freeMemory(nodesDataPtr);
wasmModule.exports.freeMemory(linksDataPtr);
wasmModule.exports.freeMemory(positionsPtr);
wasmModule.exports.freeMemory(linksTexturePtr);
wasmModule.exports.freeMemory(linkRangesTexturePtr);
// Free WASM memory in reverse allocation order (LIFO)
wasmModule.exports.freeMemory(linkRangesTexturePtr);
wasmModule.exports.freeMemory(linksTexturePtr);
wasmModule.exports.freeMemory(positionsPtr);
wasmModule.exports.freeMemory(linksDataPtr);
wasmModule.exports.freeMemory(nodesDataPtr);

Copilot uses AI. Check for mistakes.
Comment thread build/fdg.js Outdated
Comment on lines +445 to +453
// For fragments inside the circle, offset depth proportionally
if (r <= 1.0) {
// Closer to edge = larger depth offset (appears further back)
// This creates a spherical depth profile
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepth = gl_FragCoord.z + depthOffset;
} else {
gl_FragDepth = gl_FragCoord.z;
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shader writes to gl_FragDepth, which will fail to compile in WebGL1 contexts unless EXT_frag_depth is enabled and gl_FragDepthEXT is used (or depth writes are avoided). Please guard/enable the correct extension or make depth writes conditional on WebGL2 support.

Suggested change
// For fragments inside the circle, offset depth proportionally
if (r <= 1.0) {
// Closer to edge = larger depth offset (appears further back)
// This creates a spherical depth profile
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepth = gl_FragCoord.z + depthOffset;
} else {
gl_FragDepth = gl_FragCoord.z;
}
// For fragments inside the circle, offset depth proportionally
#if defined(GL_EXT_frag_depth)
if (r <= 1.0) {
// Closer to edge = larger depth offset (appears further back)
// This creates a spherical depth profile
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepthEXT = gl_FragCoord.z + depthOffset;
} else {
gl_FragDepthEXT = gl_FragCoord.z;
}
#elif __VERSION__ >= 300
if (r <= 1.0) {
// Closer to edge = larger depth offset (appears further back)
// This creates a spherical depth profile
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepth = gl_FragCoord.z + depthOffset;
} else {
gl_FragDepth = gl_FragCoord.z;
}
#else
// Fallback: do not modify fragment depth when custom depth writes
// are not supported (e.g., WebGL1 without EXT_frag_depth).
#endif

Copilot uses AI. Check for mistakes.
Comment thread src/workers/texture-worker-simple.js Outdated
Comment on lines 150 to 161
const linksTexturePtr = memoryOffset;
memoryOffset += linksTextureSize;
const linkRangesTexturePtr = memoryOffset;

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This worker writes input/output buffers starting at memoryOffset = 0. With the updated WASM now using heap.alloc internally (e.g., in processLinks), manual allocations from 0 can overlap the WASM heap region and corrupt allocations/results. Use the module’s exported allocateMemory/freeMemory (like texture-worker.js) or otherwise ensure your buffers start above the heap base and do not overlap the heap allocator’s growth area.

Copilot uses AI. Check for mistakes.
Comment thread build/fdg.js Outdated
}

gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha );
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, opacity );
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alpha is computed as opacity * t (where t includes circle AA and optionally texel.a), but the output color uses opacity instead of alpha. This effectively ignores antialiasing and texture alpha (except for the discard threshold). Use the computed alpha in the final fragment color.

Suggested change
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, opacity );
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha );

Copilot uses AI. Check for mistakes.
Comment thread build/fdg.module.js Outdated
Comment on lines +443 to +452
// Calculate custom depth to fix z-fighting with transparent points
// For fragments inside the circle, offset depth proportionally
if (r <= 1.0) {
// Closer to edge = larger depth offset (appears further back)
// This creates a spherical depth profile
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepth = gl_FragCoord.z + depthOffset;
} else {
gl_FragDepth = gl_FragCoord.z;
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shader writes to gl_FragDepth, which will fail to compile in WebGL1 contexts unless EXT_frag_depth is enabled and gl_FragDepthEXT is used (or depth writes are avoided). Please guard/enable the correct extension or make depth writes conditional on WebGL2 support.

Copilot uses AI. Check for mistakes.
Comment thread build/fdg.umd.js Outdated
}

gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha );
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, opacity );
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alpha is computed as opacity * t (where t includes circle AA and optionally texel.a), but the output color uses opacity instead of alpha. This effectively ignores antialiasing and texture alpha (except for the discard threshold). Use the computed alpha in the final fragment color.

Suggested change
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, opacity );
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha );

Copilot uses AI. Check for mistakes.
Comment thread src/workers/texture-worker.js Outdated
Comment on lines +168 to +182
const packedLinkAmount = wasmModule.exports.processTextures(
nodesDataPtr,
nodes.length,
linksDataPtr,
links.length,
textureSize,
positionsPtr,
linksTexturePtr,
linkRangesTexturePtr,
frustumSize
);

if (packedLinkAmount < 0) {
throw new Error('Packed links exceed texture capacity');
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an error is thrown after allocating WASM buffers (e.g., when packedLinkAmount < 0), the allocated pointers are never freed. This can permanently grow the WASM heap for the worker. Wrap allocation/processing/extraction in a try/finally (or similar) that always frees any successfully allocated pointers, and ensure the frees happen in the correct (reverse) order for the current allocator.

Copilot uses AI. Check for mistakes.
@claude
Copy link
Copy Markdown

claude bot commented Feb 21, 2026

Claude finished @jonobr1's task —— View job


Comprehensive PR Review: WASM Link Range & Packing Implementation

I've completed a thorough review of this PR that moves link range and packed link texture generation into WASM. Here's my detailed analysis:

Todo List:

  • Review WASM implementation in texture-processor.ts
  • Examine worker pipeline changes and memory management
  • Check error handling and fallback strategies
  • Analyze shader updates and WebGL compatibility
  • Assess security and performance implications
  • Verify fixes for issues identified in previous reviews
  • Provide comprehensive feedback summary

🟢 Significant Improvements Since Last Review

✅ Memory Management Fixed:

  • The dangerous manual memory layout from texture-worker-simple.js has been completely resolved
  • All workers now properly use allocateMemory() and freeMemory() APIs (texture-worker.js:145-149, texture-worker-simple.js:151-155)
  • Memory allocation is now safe and consistent across all worker implementations

✅ WASM Implementation Quality:

  • Well-structured AssemblyScript code with proper type safety (texture-processor.ts:21-196)
  • Comprehensive link packing algorithm that handles degree counting, offset calculation, and UV coordinate generation
  • Proper bounds checking and validation (texture-processor.ts:112-116, 146-151)

✅ Robust Architecture:

  • Excellent three-tier fallback strategy: WASM → JavaScript Worker → Main Thread
  • Consistent error handling and graceful degradation throughout the pipeline
  • Performance monitoring and memory usage tracking included

🟡 Remaining Critical Issues

1. Memory Deallocation Order (Critical)

Issue in texture-processor.ts:191-193 and similar patterns:

heap.free(cursorsPtr);        // ❌ Wrong order  
heap.free(startOffsetsPtr);   // ❌ Wrong order
heap.free(degreeCountsPtr);   // ❌ Wrong order

Problem: AssemblyScript's stub heap uses LIFO (Last-In-First-Out) deallocation. Freeing in allocation order means only the last allocation gets reclaimed, causing memory growth over time.

Fix Required: Reverse the deallocation order:

heap.free(cursorsPtr);        // ✅ Last allocated, freed first
heap.free(startOffsetsPtr);   // ✅ Middle allocation  
heap.free(degreeCountsPtr);   // ✅ First allocated, freed last

Impact: This affects all workers (texture-worker.js:205-209, texture-worker-simple.js:206-210, inline-worker-factory.js:235-239)

2. Missing Input Validation

Buffer overflow potential in multiple locations:

  • texture-processor.ts:108-111: Link validation only checks index bounds, not buffer overflow
  • texture-worker.js:154-160: No validation that input arrays match expected sizes
  • Workers trust input data without comprehensive bounds checking

Recommendation:

// Add before WASM calls:
if (nodes.length * 4 * 4 > MAX_SAFE_SIZE) {
  throw new Error(`Input too large: ${nodes.length} nodes`);
}

3. Incomplete Error Cleanup

Resource leak potential in texture-worker.js:229-236:

} catch (error) {
  // ❌ Memory allocated but never freed on error paths
  self.postMessage({ /* error response */ });
}

Fix: Wrap in try-finally or use consistent cleanup patterns to ensure memory is always freed.

🔍 Security & Performance Assessment

Security: Generally Good ✅

  • Proper WASM sandboxing maintains security boundaries
  • No direct memory access from JavaScript
  • Worker isolation prevents main thread contamination
  • Minor: Inline worker blob URLs could conflict with strict CSP policies

Performance: Excellent Optimization 🚀

  • WASM implementation should provide significant speed improvements for large graphs
  • Efficient use of transferable objects for data transfer
  • Memory-efficient processing with proper texture packing
  • Good performance monitoring and fallback metrics

Shader Compatibility: Improved ✅

The shader updates in points.js:72-90 now properly handle WebGL version differences:

  • Uses gl_FragDepthEXT for WebGL1 + EXT_frag_depth extension
  • Uses gl_FragDepth for WebGL2
  • Good: Conditional compilation prevents compilation errors

However, the final alpha calculation looks correct now (points.js:115), addressing the previous review concern.

🧪 Test Coverage Gaps

Missing test scenarios that should be added:

  • Edge cases: empty graphs, single nodes, self-loops
  • Memory exhaustion and recovery scenarios
  • WASM initialization failure handling
  • Worker termination during processing
  • Large dataset performance regression tests

📊 Architecture Assessment: Excellent

The three-tier processing strategy is well-implemented:

  1. WASM Worker (fastest) → High-performance C-like processing
  2. JavaScript Worker (medium) → Off-main-thread JS fallback
  3. Main Thread (fallback) → Last resort processing

Worker Pipeline Integration: The updates to consume linkRanges and packedLinkAmount from workers are clean and consistent across all implementations (index.js:275-278).

🏁 Final Recommendation

This is a high-quality performance enhancement with excellent architecture. The critical memory management issues from the previous review have been successfully addressed. The remaining issues are primarily around:

  1. Memory deallocation order (critical for long-term stability)
  2. Input validation (important for robustness)
  3. Error path cleanup (good defensive programming)

Priority Actions:

  1. Fix LIFO memory deallocation order in all locations
  2. Add comprehensive input validation before WASM calls
  3. Ensure error paths always clean up allocated memory
  4. Consider integration tests for the worker pipeline

The performance benefits and elegant degradation strategy make this a valuable addition. With the deallocation order fixed, this implementation will be robust and production-ready.

Overall Assessment: ✅ Approve with critical memory management fix required


@claude
Copy link
Copy Markdown

claude bot commented Feb 21, 2026

Claude finished @jonobr1's task —— View job


Comprehensive PR Review: WASM Link Range & Packing Implementation

I've completed a thorough review of this PR that moves link range and packed link texture generation into WASM. Here's my detailed analysis:

Todo List:

  • Review WASM implementation in texture-processor.ts
  • Examine worker pipeline changes and memory management
  • Check error handling and fallback strategies
  • Analyze shader updates and WebGL compatibility
  • Assess security and performance implications
  • Verify fixes for issues identified in previous reviews
  • Provide comprehensive feedback summary

🟢 Significant Improvements Since Previous Reviews

✅ Memory Management Fixed:

  • The dangerous manual memory layout from texture-worker-simple.js has been completely resolved
  • All workers now properly use allocateMemory() and freeMemory() APIs (texture-worker.js:258-262, texture-worker-simple.js:264-268)
  • Memory allocation is now safe and consistent across all worker implementations

✅ WASM Implementation Quality:

  • Well-structured AssemblyScript code with proper type safety (texture-processor.ts:21-196)
  • Comprehensive link packing algorithm that handles degree counting, offset calculation, and UV coordinate generation
  • Proper bounds checking and validation (texture-processor.ts:112-116, 146-151)

✅ Robust Architecture:

  • Excellent three-tier fallback strategy: WASM → JavaScript Worker → Main Thread
  • Consistent error handling and graceful degradation throughout the pipeline
  • Performance monitoring and memory usage tracking included

🔴 Critical Issues Found

1. Memory Deallocation Order (Critical)

Issue in texture-processor.ts:191-193 and 147-149:

heap.free(cursorsPtr);        // ❌ Wrong order  
heap.free(startOffsetsPtr);   // ❌ Wrong order
heap.free(degreeCountsPtr);   // ❌ Wrong order

Problem: AssemblyScript's stub heap uses LIFO (Last-In-First-Out) deallocation. Freeing in allocation order means only the last allocation gets reclaimed, causing memory growth over time.

Fix Required: Reverse the deallocation order:

heap.free(cursorsPtr);        // ✅ Last allocated, freed first
heap.free(startOffsetsPtr);   // ✅ Middle allocation  
heap.free(degreeCountsPtr);   // ✅ First allocated, freed last

Impact: This affects all worker memory management and will cause memory leaks in long-running applications.

2. Worker Memory Deallocation Order

Same LIFO issue in workers:

  • texture-worker.js:318-322: Also freeing in wrong order
  • texture-worker-simple.js:319-323: Same incorrect pattern

The workers should free in reverse allocation order (last allocated first).

🟡 Important Issues

3. Input Validation Gaps

Buffer overflow potential in multiple locations:

  • texture-processor.ts:108-111: Link validation only checks index bounds, not buffer overflow
  • Workers don't validate that input arrays match expected memory sizes
  • Missing comprehensive bounds checking before WASM calls

Recommendation:

// Add before WASM calls:
if (nodes.length * 4 * 4 > MAX_SAFE_SIZE) {
  throw new Error(`Input too large: ${nodes.length} nodes`);
}

4. Error Path Memory Cleanup

Resource leak potential in workers:
If errors occur after memory allocation but before processing completes, allocated memory may never be freed. The finally blocks help but don't guarantee proper LIFO order.

🟢 Excellent Implementations

Shader Compatibility: Fixed ✅

The shader updates in points.js:72-90 now properly handle WebGL version differences:

  • Uses gl_FragDepthEXT for WebGL1 + EXT_frag_depth extension
  • Uses gl_FragDepth for WebGL2
  • Good: Conditional compilation prevents compilation errors

The final alpha calculation (points.js:115) correctly uses the computed alpha value, addressing previous review concerns.

Security: Generally Secure ✅

  • Proper WASM sandboxing maintains security boundaries
  • No direct memory access from JavaScript
  • Worker isolation prevents main thread contamination
  • Minor: Inline worker blob URLs could conflict with strict CSP policies

Performance: Excellent Optimization 🚀

  • WASM implementation should provide significant speed improvements for large graphs
  • Efficient use of transferable objects for data transfer (texture-worker.js:340)
  • Memory-efficient processing with proper texture packing
  • Good performance monitoring and fallback metrics

Architecture: Well-Designed

The three-tier processing strategy is excellent:

  1. WASM Worker (fastest) → High-performance C-like processing
  2. JavaScript Worker (medium) → Off-main-thread JS fallback
  3. Main Thread (fallback) → Last resort processing

Worker Pipeline Integration: The updates to consume linkRanges and packedLinkAmount from workers are clean and consistent across all implementations (index.js:275-278).

🧪 Test Coverage Assessment

Missing test scenarios that should be added:

  • Edge cases: empty graphs, single nodes, self-loops
  • Memory exhaustion and recovery scenarios
  • WASM initialization failure handling
  • Worker termination during processing
  • Large dataset performance regression tests
  • LIFO memory deallocation edge cases

🏁 Final Recommendation

This is a high-quality performance enhancement with excellent architecture. The critical memory management issues from the previous review have been mostly addressed, but the LIFO deallocation order issue is critical for long-term stability.

Priority Actions:

  1. 🔴 CRITICAL: Fix LIFO memory deallocation order in texture-processor.ts and all workers
  2. 🟡 Important: Add comprehensive input validation before WASM calls
  3. 🟡 Important: Ensure error paths always clean up allocated memory
  4. 🟢 Nice-to-have: Add integration tests for the worker pipeline

The performance benefits and elegant degradation strategy make this a valuable addition. With the deallocation order fixed, this implementation will be robust and production-ready.

Overall Assessment: ✅ Approve with critical memory management fix required


@claude
Copy link
Copy Markdown

claude bot commented Feb 21, 2026

Claude finished @jonobr1's task —— View job


Comprehensive PR Review: WASM Link Range & Packing Implementation

Todo List:

  • Review WASM implementation in texture-processor.ts
  • Examine worker pipeline changes and memory management
  • Check error handling and fallback strategies
  • Analyze shader updates and WebGL compatibility
  • Assess security and performance implications
  • Verify fixes for issues identified in previous reviews
  • Provide comprehensive feedback summary

🟢 Significant Improvements Since Previous Reviews

✅ Memory Management Fixed:

  • The dangerous manual memory layout from texture-worker-simple.js has been completely resolved
  • All workers now properly use allocateMemory() and freeMemory() APIs (texture-worker.js:258-262, texture-worker-simple.js:264-268)
  • Memory allocation is now safe and consistent across all worker implementations

✅ WASM Implementation Quality:

  • Well-structured AssemblyScript code with proper type safety (texture-processor.ts:21-196)
  • Comprehensive link packing algorithm that handles degree counting, offset calculation, and UV coordinate generation
  • Proper bounds checking and validation (texture-processor.ts:112-116, 146-151)

✅ Robust Architecture:

  • Excellent three-tier fallback strategy: WASM → JavaScript Worker → Main Thread
  • Consistent error handling and graceful degradation throughout the pipeline
  • Performance monitoring and memory usage tracking included

🔴 Critical Issues Found

1. Memory Deallocation Order (Critical)

Issue in texture-processor.ts:191-193 and 147-149:

heap.free(cursorsPtr);        // ❌ Wrong order  
heap.free(startOffsetsPtr);   // ❌ Wrong order
heap.free(degreeCountsPtr);   // ❌ Wrong order

Problem: AssemblyScript's stub heap uses LIFO (Last-In-First-Out) deallocation. Freeing in allocation order means only the last allocation gets reclaimed, causing memory growth over time.

Fix Required: Reverse the deallocation order:

heap.free(cursorsPtr);        // ✅ Last allocated, freed first
heap.free(startOffsetsPtr);   // ✅ Middle allocation  
heap.free(degreeCountsPtr);   // ✅ First allocated, freed last

Impact: This affects all worker memory management and will cause memory leaks in long-running applications.

2. Worker Memory Deallocation Order

Same LIFO issue in workers:

  • texture-worker.js:318-322: Also freeing in wrong order
  • texture-worker-simple.js:319-323: Same incorrect pattern

The workers should free in reverse allocation order (last allocated first).

🟡 Important Issues

3. Input Validation Gaps

Buffer overflow potential in multiple locations:

  • texture-processor.ts:108-111: Link validation only checks index bounds, not buffer overflow
  • Workers don't validate that input arrays match expected memory sizes
  • Missing comprehensive bounds checking before WASM calls

Recommendation:

// Add before WASM calls:
if (nodes.length * 4 * 4 > MAX_SAFE_SIZE) {
  throw new Error(`Input too large: ${nodes.length} nodes`);
}

4. Error Path Memory Cleanup

Resource leak potential in workers:
If errors occur after memory allocation but before processing completes, allocated memory may never be freed. The finally blocks help but don't guarantee proper LIFO order.

🟢 Excellent Implementations

Shader Compatibility: Fixed ✅

The shader updates in points.js:72-90 now properly handle WebGL version differences:

  • Uses gl_FragDepthEXT for WebGL1 + EXT_frag_depth extension
  • Uses gl_FragDepth for WebGL2
  • Good: Conditional compilation prevents compilation errors

The final alpha calculation (points.js:115) correctly uses the computed alpha value, addressing previous review concerns.

Security: Generally Secure ✅

  • Proper WASM sandboxing maintains security boundaries
  • No direct memory access from JavaScript
  • Worker isolation prevents main thread contamination
  • Minor: Inline worker blob URLs could conflict with strict CSP policies

Performance: Excellent Optimization 🚀

  • WASM implementation should provide significant speed improvements for large graphs
  • Efficient use of transferable objects for data transfer (texture-worker.js:340)
  • Memory-efficient processing with proper texture packing
  • Good performance monitoring and fallback metrics

Architecture: Well-Designed

The three-tier processing strategy is excellent:

  1. WASM Worker (fastest) → High-performance C-like processing
  2. JavaScript Worker (medium) → Off-main-thread JS fallback
  3. Main Thread (fallback) → Last resort processing

Worker Pipeline Integration: The updates to consume linkRanges and packedLinkAmount from workers are clean and consistent across all implementations (index.js:275-278).

🧪 Test Coverage Assessment

Missing test scenarios that should be added:

  • Edge cases: empty graphs, single nodes, self-loops
  • Memory exhaustion and recovery scenarios
  • WASM initialization failure handling
  • Worker termination during processing
  • Large dataset performance regression tests
  • LIFO memory deallocation edge cases

🏁 Final Recommendation

This is a high-quality performance enhancement with excellent architecture. The critical memory management issues from the previous review have been mostly addressed, but the LIFO deallocation order issue is critical for long-term stability.

Priority Actions:

  1. 🔴 CRITICAL: Fix LIFO memory deallocation order in texture-processor.ts and all workers
  2. 🟡 Important: Add comprehensive input validation before WASM calls
  3. 🟡 Important: Ensure error paths always clean up allocated memory
  4. 🟢 Nice-to-have: Add integration tests for the worker pipeline

The performance benefits and elegant degradation strategy make this a valuable addition. With the deallocation order fixed, this implementation will be robust and production-ready.

Overall Assessment: ✅ Approve with critical memory management fix required


@jonobr1
Copy link
Copy Markdown
Owner Author

jonobr1 commented Feb 21, 2026

Addressed on branch :\n\n- LIFO deallocation order is fixed in WASM link packing:\n - \n - \n- Worker allocation/free ordering and error-path cleanup are fixed with + reverse frees:\n - and \n - and \n - and \n- Broader validation and typed error handling added (//):\n - \n - \n - \n- Regression tests added and passing (

@jonobr1/force-directed-graph@1.21.6 test
vitest run

RUN v4.0.18 /Users/jonobrandel/Documents/force-directed-graph

✓ src/workers/tests/texture-worker.unit.test.js (5 tests) 2ms
✓ src/workers/tests/texture-worker.integration.test.js (2 tests) 2ms

Test Files 2 passed (2)
Tests 7 passed (7)
Start at 19:19:20
Duration 84ms (transform 24ms, setup 0ms, import 46ms, tests 4ms, environment 0ms)):\n - \n - \n\nRelated commits: , , .

@jonobr1
Copy link
Copy Markdown
Owner Author

jonobr1 commented Feb 21, 2026

Correction to my previous note (shell escaping ate markdown):

Addressed on branch codex/wasm-link-range-packing:

  • LIFO deallocation order is fixed in WASM link packing:
    • /Users/jonobrandel/Documents/force-directed-graph/src/wasm/texture-processor.ts:147
    • /Users/jonobrandel/Documents/force-directed-graph/src/wasm/texture-processor.ts:191
  • Worker allocation/free ordering and error-path cleanup are fixed with try/finally + reverse frees:
    • /Users/jonobrandel/Documents/force-directed-graph/src/workers/texture-worker.js:258 and /Users/jonobrandel/Documents/force-directed-graph/src/workers/texture-worker.js:318
    • /Users/jonobrandel/Documents/force-directed-graph/src/workers/texture-worker-simple.js:264 and /Users/jonobrandel/Documents/force-directed-graph/src/workers/texture-worker-simple.js:319
    • /Users/jonobrandel/Documents/force-directed-graph/src/inline-worker-factory.js:292 and /Users/jonobrandel/Documents/force-directed-graph/src/inline-worker-factory.js:348
  • Broader validation and typed error handling added (validation/memory/processing):
    • /Users/jonobrandel/Documents/force-directed-graph/src/workers/texture-worker.js:118
    • /Users/jonobrandel/Documents/force-directed-graph/src/workers/texture-worker-simple.js:116
    • /Users/jonobrandel/Documents/force-directed-graph/src/inline-worker-factory.js:122
  • Regression tests added and passing (npm test):
    • /Users/jonobrandel/Documents/force-directed-graph/src/workers/tests/texture-worker.unit.test.js
    • /Users/jonobrandel/Documents/force-directed-graph/src/workers/tests/texture-worker.integration.test.js

Related commits: b88e096, 3d2c7ea, 382774d.

@jonobr1 jonobr1 merged commit b3afab0 into main Feb 21, 2026
1 check passed
@jonobr1 jonobr1 deleted the codex/wasm-link-range-packing branch February 21, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants