Initial release: Go Jdenticon library v0.1.0

- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
This commit is contained in:
Kevin McIntyre
2026-01-02 23:56:48 -05:00
parent f84b511895
commit d9e84812ff
292 changed files with 19725 additions and 38884 deletions

140
benchmark/benchmark-js.js Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env node
/**
* Node.js benchmark script for jdenticon-js using perf_hooks
* Tests performance with JIT warm-up for fair comparison against Go implementation
* Run with: node --expose-gc benchmark-js.js
*/
const fs = require('fs');
const path = require('path');
const { performance } = require('perf_hooks');
// Check if jdenticon-js is available
let jdenticon;
try {
jdenticon = require('../jdenticon-js/dist/jdenticon-node.js');
} catch (err) {
console.error('Error: jdenticon-js not found. Please ensure it\'s available in jdenticon-js/dist/');
console.error('You may need to build the JS version first.');
process.exit(1);
}
// --- Configuration ---
const ICON_SIZE = 64;
const WARMUP_RUNS = 3;
// Load test inputs
const inputsPath = path.join(__dirname, 'inputs.json');
if (!fs.existsSync(inputsPath)) {
console.error('Error: inputs.json not found. Run generate-inputs.js first.');
process.exit(1);
}
const inputs = JSON.parse(fs.readFileSync(inputsPath, 'utf8'));
const numInputs = inputs.length;
console.log('=== jdenticon-js Performance Benchmark ===');
console.log(`Inputs: ${numInputs} unique hash strings`);
console.log(`Icon size: ${ICON_SIZE}x${ICON_SIZE} pixels`);
console.log(`Format: SVG`);
console.log(`Node.js version: ${process.version}`);
console.log(`V8 version: ${process.versions.v8}`);
console.log('');
// --- Benchmark Function ---
function generateAllIcons() {
for (let i = 0; i < numInputs; i++) {
jdenticon.toSvg(inputs[i], ICON_SIZE);
}
}
// --- Warm-up Phase ---
console.log(`Warming up JIT with ${WARMUP_RUNS} runs...`);
for (let i = 0; i < WARMUP_RUNS; i++) {
console.log(` Warm-up run ${i + 1}/${WARMUP_RUNS}`);
generateAllIcons();
}
// Force garbage collection for clean baseline (if --expose-gc was used)
if (global.gc) {
console.log('Forcing garbage collection...');
global.gc();
} else {
console.log('Note: Run with --expose-gc for more accurate memory measurements');
}
console.log('');
// --- Measurement Phase ---
console.log(`Running benchmark with ${numInputs} icons...`);
const memBefore = process.memoryUsage();
const startTime = performance.now();
generateAllIcons(); // The actual benchmark run
const endTime = performance.now();
const memAfter = process.memoryUsage();
// --- Calculate Metrics ---
const totalTimeMs = endTime - startTime;
const timePerIconMs = totalTimeMs / numInputs;
const timePerIconUs = timePerIconMs * 1000; // microseconds
const iconsPerSecond = 1000 / timePerIconMs;
// Memory metrics
const heapDelta = memAfter.heapUsed - memBefore.heapUsed;
const heapDeltaKB = heapDelta / 1024;
const heapDeltaPerIcon = heapDelta / numInputs;
// --- Results Report ---
console.log('');
console.log('=== jdenticon-js Results ===');
console.log(`Total time: ${totalTimeMs.toFixed(2)} ms`);
console.log(`Time per icon: ${timePerIconMs.toFixed(4)} ms (${timePerIconUs.toFixed(2)} μs)`);
console.log(`Throughput: ${iconsPerSecond.toFixed(2)} icons/sec`);
console.log('');
console.log('Memory Usage:');
console.log(` Heap before: ${(memBefore.heapUsed / 1024).toFixed(2)} KB`);
console.log(` Heap after: ${(memAfter.heapUsed / 1024).toFixed(2)} KB`);
console.log(` Heap delta: ${heapDeltaKB.toFixed(2)} KB`);
console.log(` Per icon: ${heapDeltaPerIcon.toFixed(2)} bytes`);
console.log('');
console.log('Additional Memory Info:');
console.log(` RSS: ${(memAfter.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(` External: ${(memAfter.external / 1024).toFixed(2)} KB`);
// --- Save Results for Comparison ---
const results = {
implementation: 'jdenticon-js',
timestamp: new Date().toISOString(),
nodeVersion: process.version,
v8Version: process.versions.v8,
config: {
iconSize: ICON_SIZE,
numInputs: numInputs,
warmupRuns: WARMUP_RUNS
},
performance: {
totalTimeMs: totalTimeMs,
timePerIconMs: timePerIconMs,
timePerIconUs: timePerIconUs,
iconsPerSecond: iconsPerSecond
},
memory: {
heapBeforeKB: memBefore.heapUsed / 1024,
heapAfterKB: memAfter.heapUsed / 1024,
heapDeltaKB: heapDeltaKB,
heapDeltaPerIcon: heapDeltaPerIcon,
rssKB: memAfter.rss / 1024,
externalKB: memAfter.external / 1024
}
};
const resultsPath = path.join(__dirname, 'results-js.json');
fs.writeFileSync(resultsPath, JSON.stringify(results, null, 2));
console.log(`Results saved to: ${resultsPath}`);
console.log('');
console.log('Run Go benchmark next: go test -bench=BenchmarkGenerate64pxIcon -benchmem ./...');

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env node
/**
* Compare benchmark results between Go and JavaScript implementations
*/
const fs = require('fs');
// Read results
const jsResults = JSON.parse(fs.readFileSync('./results-js.json', 'utf8'));
const goResults = JSON.parse(fs.readFileSync('./results-go.json', 'utf8'));
console.log('=== jdenticon-js vs go-jdenticon Performance Comparison ===\n');
console.log('Environment:');
console.log(` JavaScript: Node.js ${jsResults.nodeVersion}, V8 ${jsResults.v8Version}`);
console.log(` Go: ${goResults.goVersion}`);
console.log(` Test inputs: ${jsResults.config.numInputs} unique hash strings`);
console.log(` Icon size: ${jsResults.config.iconSize}x${jsResults.config.iconSize} pixels\n`);
console.log('Performance Results:');
console.log('┌─────────────────────────┬─────────────────┬─────────────────┬──────────────────┐');
console.log('│ Metric │ jdenticon-js │ go-jdenticon │ Go vs JS │');
console.log('├─────────────────────────┼─────────────────┼─────────────────┼──────────────────┤');
// Time per icon
const jsTimeMs = jsResults.performance.timePerIconMs;
const goTimeMs = goResults.performance.timePerIconMs;
const timeDelta = ((goTimeMs - jsTimeMs) / jsTimeMs * 100);
const timeComparison = timeDelta > 0 ? `+${timeDelta.toFixed(1)}% slower` : `${Math.abs(timeDelta).toFixed(1)}% faster`;
console.log(`│ Time/Icon (ms) │ ${jsTimeMs.toFixed(4).padStart(15)}${goTimeMs.toFixed(4).padStart(15)}${timeComparison.padStart(16)}`);
// Throughput
const jsThroughput = jsResults.performance.iconsPerSecond;
const goThroughput = goResults.performance.iconsPerSecond;
const throughputDelta = ((goThroughput - jsThroughput) / jsThroughput * 100);
const throughputComparison = throughputDelta > 0 ? `+${throughputDelta.toFixed(1)}% faster` : `${Math.abs(throughputDelta).toFixed(1)}% slower`;
console.log(`│ Throughput (icons/sec) │ ${jsThroughput.toFixed(2).padStart(15)}${goThroughput.toFixed(2).padStart(15)}${throughputComparison.padStart(16)}`);
// Memory - Report side-by-side without direct comparison (different methodologies)
const jsMemoryKB = jsResults.memory.heapDeltaKB;
const goMemoryB = goResults.memory.bytesPerOp;
console.log(`│ JS Heap Delta (KB) │ ${jsMemoryKB.toFixed(2).padStart(15)}${'-'.padStart(15)}${'N/A'.padStart(16)}`);
console.log(`│ Go Allocs/Op (bytes) │ ${'-'.padStart(15)}${goMemoryB.toString().padStart(15)}${'N/A'.padStart(16)}`);
console.log('└─────────────────────────┴─────────────────┴─────────────────┴──────────────────┘\n');
console.log('Additional Go Metrics:');
console.log(` Allocations per icon: ${goResults.memory.allocsPerOp} allocs`);
console.log(` Benchmark iterations: ${goResults.config.benchmarkIterations}\n`);
console.log('Summary:');
const faster = timeDelta < 0 ? 'Go' : 'JavaScript';
const fasterPercent = Math.abs(timeDelta).toFixed(1);
console.log(`${faster} is ${fasterPercent}% faster in CPU time`);
const targetMet = Math.abs(timeDelta) <= 20;
const targetStatus = targetMet ? 'MEETS' : 'DOES NOT MEET';
console.log(`• Go implementation ${targetStatus} the target of being within 20% of jdenticon-js performance`);
console.log(`• JS shows a heap increase of ${jsMemoryKB.toFixed(0)} KB for the batch of ${jsResults.config.numInputs} icons`);
console.log(`• Go allocates ${goMemoryB} bytes per icon generation (different measurement methodology)`);
console.log(`• Go has excellent memory allocation profile with only ${goResults.memory.allocsPerOp} allocations per icon`);
if (targetMet) {
console.log('\n✅ Performance target achieved! Go implementation is competitive with JavaScript.');
} else {
console.log('\n⚠ Performance target not met. Consider optimization opportunities.');
}

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env node
/**
* Correctness test to verify Go and JS implementations produce identical SVG output
* This must pass before performance benchmarks are meaningful
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Check if jdenticon-js is available
let jdenticon;
try {
jdenticon = require('../jdenticon-js/dist/jdenticon-node.js');
} catch (err) {
console.error('Error: jdenticon-js not found. Please ensure it\'s available in jdenticon-js/dist/');
console.error('You may need to build the JS version first.');
process.exit(1);
}
// Test inputs for correctness verification
const testInputs = [
'user@example.com',
'test-hash-123',
'benchmark-input-abc',
'empty-string',
'special-chars-!@#$%'
];
const ICON_SIZE = 64;
console.log('Running correctness test...');
console.log('Verifying Go and JS implementations produce identical SVG output\n');
// Create temporary directory for outputs
const tempDir = './temp-correctness';
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir);
}
let allPassed = true;
for (let i = 0; i < testInputs.length; i++) {
const input = testInputs[i];
console.log(`Testing input ${i + 1}/${testInputs.length}: "${input}"`);
try {
// Generate SVG using JavaScript implementation
const jsSvg = jdenticon.toSvg(input, ICON_SIZE);
const jsPath = path.join(tempDir, `js-${i}.svg`);
fs.writeFileSync(jsPath, jsSvg);
// Generate SVG using Go implementation via CLI
const goPath = path.join(tempDir, `go-${i}.svg`);
const absoluteGoPath = path.resolve(goPath);
const goCommand = `cd .. && go run cmd/jdenticon/main.go -value="${input}" -size=${ICON_SIZE} -format=svg -output="${absoluteGoPath}"`;
try {
execSync(goCommand, { stdio: 'pipe' });
} catch (goErr) {
console.error(` ❌ Failed to generate Go SVG: ${goErr.message}`);
allPassed = false;
continue;
}
// Read Go-generated SVG
if (!fs.existsSync(goPath)) {
console.error(` ❌ Go SVG file not created: ${goPath}`);
allPassed = false;
continue;
}
const goSvg = fs.readFileSync(goPath, 'utf8');
// Compare SVGs
if (jsSvg === goSvg) {
console.log(` ✅ PASS - SVGs are identical`);
} else {
console.log(` ❌ FAIL - SVGs differ`);
console.log(` JS length: ${jsSvg.length} chars`);
console.log(` Go length: ${goSvg.length} chars`);
// Save diff for analysis
const diffPath = path.join(tempDir, `diff-${i}.txt`);
fs.writeFileSync(diffPath, `JS SVG:\n${jsSvg}\n\nGo SVG:\n${goSvg}\n`);
console.log(` Diff saved to: ${diffPath}`);
allPassed = false;
}
} catch (err) {
console.error(` ❌ Error testing input "${input}": ${err.message}`);
allPassed = false;
}
console.log('');
}
// Clean up temporary files if all tests passed
if (allPassed) {
try {
fs.rmSync(tempDir, { recursive: true });
console.log('✅ All correctness tests PASSED - SVG outputs are identical!');
console.log(' Performance benchmarks will be meaningful.');
} catch (cleanupErr) {
console.log('✅ All correctness tests PASSED (cleanup failed)');
}
} else {
console.log('❌ Some correctness tests FAILED');
console.log(` Review differences in ${tempDir}/`);
console.log(' Fix Go implementation before running performance benchmarks.');
process.exit(1);
}

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env node
/**
* Generate consistent test inputs for jdenticon benchmarking
* Creates deterministic hash strings for fair comparison between Go and JS implementations
*/
const fs = require('fs');
const crypto = require('crypto');
const NUM_INPUTS = 1000;
const inputs = [];
// Generate deterministic inputs by hashing incremental strings
for (let i = 0; i < NUM_INPUTS; i++) {
// Use a variety of input types to make the benchmark realistic
let input;
if (i % 4 === 0) {
input = `user${i}@example.com`;
} else if (i % 4 === 1) {
input = `test-hash-${i}`;
} else if (i % 4 === 2) {
input = `benchmark-input-${i.toString(16)}`;
} else {
// Use a deterministic source for the "random" part
const randomPart = crypto.createHash('sha1').update(`seed-${i}`).digest('hex').substring(0, 12);
input = `random-string-${randomPart}`;
}
// Generate SHA1 hash (same as jdenticon uses)
const hash = crypto.createHash('sha1').update(input).digest('hex');
inputs.push(hash);
}
// Write inputs to JSON file
const outputPath = './inputs.json';
fs.writeFileSync(outputPath, JSON.stringify(inputs, null, 2));
console.log(`Generated ${NUM_INPUTS} test inputs and saved to ${outputPath}`);
console.log(`Sample inputs:`);
console.log(inputs.slice(0, 5));

1002
benchmark/inputs.json Normal file

File diff suppressed because it is too large Load Diff

58
benchmark/run-go-benchmark.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# Run Go benchmark and save results
echo "=== Go jdenticon Performance Benchmark ==="
echo "Running Go benchmark..."
# Run the benchmark and capture output
BENCHMARK_OUTPUT=$(cd .. && go test -run="^$" -bench=BenchmarkGenerate64pxIcon -benchmem ./jdenticon 2>&1)
echo "$BENCHMARK_OUTPUT"
# Parse benchmark results
# Example output: BenchmarkGenerate64pxIcon-10 92173 12492 ns/op 13174 B/op 239 allocs/op
BENCHMARK_LINE=$(echo "$BENCHMARK_OUTPUT" | grep BenchmarkGenerate64pxIcon)
ITERATIONS=$(echo "$BENCHMARK_LINE" | awk '{print $2}')
NS_PER_OP=$(echo "$BENCHMARK_LINE" | awk '{print $3}')
BYTES_PER_OP=$(echo "$BENCHMARK_LINE" | awk '{print $5}')
ALLOCS_PER_OP=$(echo "$BENCHMARK_LINE" | awk '{print $7}')
# Convert nanoseconds to milliseconds and microseconds
TIME_PER_ICON_MS=$(echo "scale=4; $NS_PER_OP / 1000000" | bc | awk '{printf "%.4f", $0}')
TIME_PER_ICON_US=$(echo "scale=2; $NS_PER_OP / 1000" | bc | awk '{printf "%.2f", $0}')
ICONS_PER_SECOND=$(echo "scale=2; 1000000000 / $NS_PER_OP" | bc)
echo ""
echo "=== go-jdenticon Results ==="
echo "Iterations: $ITERATIONS"
echo "Time per icon: ${TIME_PER_ICON_MS} ms (${TIME_PER_ICON_US} μs)"
echo "Throughput: ${ICONS_PER_SECOND} icons/sec"
echo "Memory per icon: $BYTES_PER_OP bytes"
echo "Allocations per icon: $ALLOCS_PER_OP allocs"
# Create JSON results
cat > results-go.json << EOF
{
"implementation": "go-jdenticon",
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")",
"goVersion": "$(go version | awk '{print $3}')",
"config": {
"iconSize": 64,
"numInputs": 1000,
"benchmarkIterations": $ITERATIONS
},
"performance": {
"nsPerOp": $NS_PER_OP,
"timePerIconMs": $TIME_PER_ICON_MS,
"timePerIconUs": $TIME_PER_ICON_US,
"iconsPerSecond": $ICONS_PER_SECOND
},
"memory": {
"bytesPerOp": $(echo "$BYTES_PER_OP" | sed 's/[^0-9]//g'),
"allocsPerOp": $(echo "$ALLOCS_PER_OP" | sed 's/[^0-9]//g')
}
}
EOF
echo ""
echo "Results saved to: results-go.json"