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:
140
benchmark/benchmark-js.js
Normal file
140
benchmark/benchmark-js.js
Normal 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 ./...');
|
||||
72
benchmark/compare-results.js
Normal file
72
benchmark/compare-results.js
Normal 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.');
|
||||
}
|
||||
114
benchmark/correctness-test.js
Normal file
114
benchmark/correctness-test.js
Normal 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);
|
||||
}
|
||||
41
benchmark/generate-inputs.js
Normal file
41
benchmark/generate-inputs.js
Normal 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
1002
benchmark/inputs.json
Normal file
File diff suppressed because it is too large
Load Diff
58
benchmark/run-go-benchmark.sh
Executable file
58
benchmark/run-go-benchmark.sh
Executable 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"
|
||||
Reference in New Issue
Block a user