From: Chris Duncan Date: Sat, 25 Jul 2026 21:27:03 +0000 (-0700) Subject: Overhaul testing script. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=ca0d0f014a283cf62009a73d582a00bd7f2381e0;p=nano-pow.git Overhaul testing script. --- diff --git a/index.html b/index.html index d88bfc6..e88f563 100644 --- a/index.html +++ b/index.html @@ -28,13 +28,7 @@ SPDX-License-Identifier: GPL-3.0-or-later const logger = new Logger() - if (NanoPow) { - document.getElementById('btnStartTest').disabled = false - document.getElementById('status').innerHTML = `READY` - } - - try { - const glSize = (canvas => { + const glSize = (canvas => { const gl = canvas.getContext('webgl2') const MAX_VIEWPORT_DIMS = gl?.getParameter(gl.MAX_VIEWPORT_DIMS) ?? [0x1000, 0x1000] const size = Math.min(0x1000, ...MAX_VIEWPORT_DIMS) @@ -42,106 +36,133 @@ SPDX-License-Identifier: GPL-3.0-or-later return gl?.drawingBufferHeight < gl?.drawingBufferWidth ? gl?.drawingBufferHeight : gl?.drawingBufferWidth })(new OffscreenCanvas(0, 0)) + if (NanoPow) { + document.getElementById('btnStartTest').disabled = false + document.getElementById('status').innerHTML = `READY` + } + function random (length = 32) { const bytes = crypto.getRandomValues(new Uint8Array(length)) return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('') } - function average(times, type, effort) { - const averages = stats(times) - const title = type === 'WebGPU' - ? `NanoPow (${type}) | Effort: ${effort} | Dispatch: ${(effort << 8) << 1} | Threads: ${64 * (effort << 8) << 1}` - : type === 'WebGL' - ? `NanoPow (${type}) | Effort: ${effort} | Work per frame: ${Math.min(effort << 8, glSize) ** 2}` - : `NanoPow (${type}) | Effort: ${effort}` - return { - [title]: averages - } + function average(times, type, effort) { + const averages = stats(times) + const title = type === 'WebGPU' + ? `NanoPow (${type}) | Effort: ${effort} | Dispatch: ${(effort << 8) << 1} | Threads: ${64 * (effort << 8) << 1}` + : type === 'WebGL' + ? `NanoPow (${type}) | Effort: ${effort} | Work per frame: ${Math.min(effort << 8, glSize) ** 2}` + : `NanoPow (${type}) | Effort: ${effort}` + return { + [title]: averages } + } - async function sequenceGenerateValidate (hash, api, debug, difficulty, effort) { - logger.isEnabled = debug - let start = 0, end = 0, work = null, valid = false - - start = performance.now() - work = (await NanoPow.work_generate(hash, {api, debug, difficulty, effort})).work - valid = (await NanoPow.work_validate(work, hash, {api, debug, difficulty, effort})).valid - end = performance.now() - - if (!valid) { - throw new Error(`invalid result\nblock hash: ${hash}\nwork: ${work}`) - } - if (!start || !end || start > end) { - throw new Error(`invalid timing\nstart: ${start}\nend: ${end}`) - } - return end - start + /** + * @param {string} api + * @param {string} difficulty + * @param {number} effort + * @param {boolean }debug + */ + async function sequenceGenerateValidate (api, difficulty, effort, debug) { + let start = 0, end = 0, duration = 0, result = null, check = false + const hash = random() + + start = performance.now() + result = await NanoPow.work_generate(hash, {api, debug, difficulty, effort}) + check = await NanoPow.work_validate(result.work, result.hash, {api, debug, difficulty, effort}) + end = performance.now() + duration = end - start + + const valid = (result.hash === hash && check.hash === hash && check.valid === '1') + document.getElementById('output').textContent += `${valid ? 'VALID' : 'INVALID'} (${duration} ms)\n${JSON.stringify(result, ' ', 2)}\n` + if (!valid) { + logger.log(hash, result, check) + throw new Error(`invalid result\nblock hash: ${hash}\nwork: ${work}`) } + if (!start || !end || start > end) { + throw new Error(`invalid timing\nstart: ${start}\nend: ${end}`) + } + return duration + } - async function run(size, difficulty, effort, api, isOutputShown, isDebug, isSelfCheck) { - logger.isEnabled = isDebug - // Generate once on load to compile shaders and initialize buffers - await NanoPow.work_generate(random(), { api, difficulty: '0', debug: isDebug }) - const type = api - api = type.toLowerCase() - if (isSelfCheck) { - Cache.clear() - document.getElementById('status').innerHTML = `RUNNING SELF-CHECK` - logger.log(`%cNanoPow`, 'color:green', 'Checking validation against known values') + /** + * @param {string} name + * @param {boolean} test + */ + function check (name, test) { + if (typeof test !== 'boolean') { + throw 'invalid test' + } + console.log(`%c${test ? 'PASS' : 'FAIL'}%c: ${name}`, `color:${test ? 'green' : 'red'}`, 'color:unset') + } - const expect = [] - let result + /** + * @param {string} api + * @param {boolean} debug + */ + async function selfCheck (api, debug) { + document.getElementById('status').innerHTML = `RUNNING SELF-CHECK` + logger.log(`%cNanoPow`, 'color:green', 'Checking validation against known values') + const options = { api, debug } + // Generate once on load to compile shaders and initialize buffers + await NanoPow.work_generate(random(), { ...options, difficulty: '0' }) + + Cache.clear() + const expect = [] + let result // PASS - result = await NanoPow.work_validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', { api, debug: isDebug }) + result = await NanoPow.work_validate('47c83266398728cf', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', options) logger.log(result) result = result.valid_all === '1' logger.log(`work_validate() output for good nonce 1 is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', { api, debug: isDebug }) + result = await NanoPow.work_validate('4a8fb104eebbd336', '8797585D56B8AEA3A62899C31FC088F9BE849BA8298A88E94F6E3112D4E55D01', options) logger.log(result) result = result.valid_all === '1' logger.log(`work_validate() output for good nonce 2 is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', { api, debug: isDebug }) + result = await NanoPow.work_validate('c5d5d6f7c5d6ccd1', '281E89AC73B1082B464B9C3C1168384F846D39F6DF25105F8B4A22915E999117', options) logger.log(result) result = result.valid_all === '1' logger.log(`work_validate() output for colliding nonce is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { api, difficulty: 0xfffffe0000000000n, debug: isDebug }) + result = await NanoPow.work_validate('6866c1ac3831a891', '7069D9CD1E85D6204301D254B0927F06ACC794C9EA5DF70EA5578458FB597090', { ...options, difficulty: 0xfffffe0000000000n }) logger.log(result) result = result.valid === '1' && result.valid_all === '0' && result.valid_receive === '1' logger.log(`work_validate() output for good receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) // XFAIL - result = await NanoPow.work_validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', { api, debug: isDebug }) + result = await NanoPow.work_validate('0000000000000000', '0000000000000000000000000000000000000000000000000000000000000000', options) logger.log(result) result = result.valid_all === '0' logger.log(`work_validate() output for bad nonce 1 is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', { api, debug: isDebug }) + result = await NanoPow.work_validate('c5d5d6f7c5d6ccd1', 'BA1E946BA3D778C2F30A83D44D2132CC6EEF010D8D06FF10A8ABD0100D8FB47E', options) logger.log(result) result = result.valid_all === '0' logger.log(`work_validate() output for bad nonce 2 is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { api, difficulty: 0xfffffff800000001n, debug: isDebug }) + result = await NanoPow.work_validate('ae238556213c3624', 'BF41D87DA3057FDC6050D2B00C06531F89F4AA6195D7C6C2EAAF15B6E703F8F6', { ...options, difficulty: 0xfffffff800000001n }) logger.log(result) result = result.error === 'Invalid difficulty fffffff800000001' logger.log(`work_validate() output for bad difficulty beyond max is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', { api, debug: isDebug }) + result = await NanoPow.work_validate('29a9ae0236990e2e', '32721F4BD2AFB6F6A08D41CD0DF3C0D9C0B5294F68D0D12422F52B28F0800B5F', options) logger.log(result) result = result.valid_all === '0' logger.log(`work_validate() output for slightly wrong nonce is ${result === true ? 'correct' : 'incorrect'}`) expect.push(result) - result = await NanoPow.work_validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { api, difficulty: 0xfffffe0000000000n, debug: isDebug }) + result = await NanoPow.work_validate('7d903b18d03f9820', '39C57C28F904DFE4012288FFF64CE80C0F42601023A9C82108E8F7B2D186C150', { ...options, difficulty: 0xfffffe0000000000n }) logger.log(result) result = result.valid === '0' && result.valid_all === '0' && result.valid_receive === '0' logger.log(`work_validate() output for bad receive difficulty nonce is ${result === true ? 'correct' : 'incorrect'}`) @@ -155,176 +176,131 @@ SPDX-License-Identifier: GPL-3.0-or-later ' ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn ', 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' ] - for (let i = 0; i < prefixes.length; i++) { - const hash = prefixes[i] - let result = null - const start = performance.now() - try { - result = await NanoPow.work_generate(hash, { api, difficulty, effort, debug: isDebug }) - logger.log(result) - } catch (err) { - document.getElementById('output').innerHTML += `Error: ${err.message}
${err.cause}
${err.stack}` - console.error(err) - return - } - const end = performance.now() - const check = await NanoPow.work_validate(result.work, result.hash, { difficulty, debug: isDebug }) - const isValid = (check.valid === '1' && BigInt(`0x${result.hash}`) === BigInt(`0x${hash.trim().replace('n', '').replace(/^0x/i, '')}`)) - logger.log(`work_generate() output for max value block hash is ${isValid === true ? 'correct' : 'incorrect'}`) - expect.push(isValid) - } - - try { - if (!expect.every(result => result)) throw new Error(`Validation is not working`) - } catch (err) { - document.getElementById('status').innerHTML = `FAILED TO VALIDATE KNOWN VALUES` - document.getElementById('output').innerHTML += `Error: ${err.message}
` - console.error(err) - return - } - } - - logger.log(`%cNanoPow (${type})`, 'color:green', `Calculate proof-of-work for ${size} unique send block hashes`) - document.getElementById('status').innerHTML = `TESTING IN PROGRESS 0/${size}
` - let times + for (let i = 0; i < prefixes.length; i++) { + const hash = prefixes[i] try { - times = await execute(size, difficulty, effort, api, isOutputShown, isDebug) + const result = await NanoPow.work_generate(hash, options) + logger.log(result) } catch (err) { - output.textContent += `Error: ${err.message}` - output.innerHTML += '
' + document.getElementById('output').innerHTML += `Error: ${err.message}
${err.cause}
${err.stack}` console.error(err) return } - document.getElementById('output').innerHTML += `
` - document.getElementById('summary').innerHTML += `${JSON.stringify(average(times, type, effort), null, '\t')}
` - document.getElementById('status').innerHTML = `TESTING COMPLETE
` - logger.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold') + const end = performance.now() + const check = await NanoPow.work_validate(result.work, result.hash, options) + const isValid = (check.valid === '1' && BigInt(`0x${result.hash}`) === BigInt(`0x${hash.trim().replace('n', '').replace(/^0x/i, '')}`)) + logger.log(`work_generate() output for max value block hash is ${isValid === true ? 'correct' : 'incorrect'}`) + expect.push(isValid) } - async function score(runs, size, difficulty, effort, api) { - logger.log(`%cNanoPow ${api}`, 'color:green', `Calculate truncated harmonic mean of the truncated arithmetic rate across ${runs} runs of ${size} samples.`) - const rates = [] - for (let i = 0; i < runs; i++) { - let times - document.getElementById('status').innerHTML = `SCORING IN PROGRESS. THIS WILL TAKE A LONG TIME. ${i}/${runs} 0/${size}
` + if (!expect.every(result => result)) { + document.getElementById('status').innerHTML = `FAILED TO VALIDATE KNOWN VALUES` + document.getElementById('output').innerHTML += `Error: ${err.message}
` + console.error(err) + throw new Error(`Validation is not working`) + } + } + + /** + * @param {string} api + * @param {string} difficulty + * @param {number} effort + * @param {boolean} debug + * @param {number} size + * @param {number} runs + */ + async function test (api, difficulty, effort, debug, size, runs) { + logger.isEnabled = debug + const type = api + api = type.toLowerCase() + selfCheck(api, debug) + Cache.clear() + logger.log(`%cNanoPow ${type}`, 'color:green', `Calculate truncated harmonic mean of the truncated arithmetic rate of generating proof-of-work across ${runs} runs of ${size} samples.`) + const rates = [] + for (let i = 0; i < runs; i++) { + await new Promise(r => setTimeout(r)) + const times = [] + for (let j = 0; j < size; j++) { + document.getElementById('status').innerHTML = `TESTING IN PROGRESS. THIS MAY TAKE A LONG TIME. ${i}/${runs} ${j}/${size}
` try { - times = await execute(size, difficulty, effort, api) + const duration = await sequenceGenerateValidate(api, difficulty, effort, debug) + times.push(duration) } catch (err) { document.getElementById('output').innerHTML += `Error: ${err.message}
` console.error(err) return } - const results = Object.values(average(times))[0] - const { truncatedRate } = results - rates.push(truncatedRate) - document.getElementById('output').innerHTML += `Benchmark ${i + 1} score: ${truncatedRate} wps
` } - const results = Object.values(average(rates))[0] - logger.log(rates) - logger.log(results) - const { truncatedHarmonic } = results - document.getElementById('output').innerHTML += `
` - document.getElementById('summary').innerHTML += `work-per-second: ${truncatedHarmonic}
` - document.getElementById('status').innerHTML = `SCORING COMPLETE
` - logger.log('%cSCORING COMPLETE', 'color:orange;font-weight:bold') - } - - async function execute(size, difficulty, effort, api, isOutputShown = false, debug = false) { - Cache.clear() - const output = document.getElementById('output') - const status = document.getElementById('status') - const times = [] - let hash, result, start, end, check, isValid - for (let i = 0; i < size; i++) { - const prev = RegExp(`${(i || 1) - 1}/${size}$`) - const next = `${i}/${size}` - status.textContent = status.textContent.replace(prev, next) - hash = random() - start = performance.now() - result = await NanoPow.work_generate(hash, { difficulty, effort, api, debug }) - end = performance.now() - times.push(end - start) - check = await NanoPow.work_validate(result.work, result.hash, { difficulty, debug }) - isValid = (result.hash === hash && check.hash === hash && check.valid === '1') - if (isOutputShown) { - output.textContent += `${isValid ? 'VALID' : 'INVALID'} (${end - start} ms)\n${JSON.stringify(result, ' ', 2)}\n` - if (!isValid) logger.log(hash, result, check) - } - } - return times + const avg = average(times, api, effort) + const result = Object.values(avg)[0] + const { truncatedRate } = result + rates.push(truncatedRate) + document.getElementById('summary').innerHTML += `Test run ${i + 1} score: ${truncatedRate} wps
` + document.getElementById('output').innerHTML += `${JSON.stringify(avg, null, '\t')}
` } + const results = stats(rates) + const { truncatedHarmonic } = results + logger.log(rates) + logger.log(results) + document.getElementById('output').innerHTML += `
` + document.getElementById('summary').innerHTML += `work-per-second: ${truncatedHarmonic}
` + document.getElementById('status').innerHTML = `TESTING COMPLETE
` + logger.log('%TESTING COMPLETE', 'color:orange;font-weight:bold') + } - function startValidation(event) { - const difficulty = document.getElementById('difficulty')?.value - const work = document.getElementById('work')?.value - const hash = document.getElementById('hash')?.value - const validation = document.getElementById('validation') - const api = document.getElementById('api')?.value - const apiContainer = document.getElementById('api')?.parentElement - if (api === 'CPU') { - apiContainer.classList.add('warning') - apiContainer.title = 'Use only for testing with very low difficulty.' - } else { - apiContainer.classList.remove('warning') - apiContainer.title = '' - } - validation.innerText = `\u23f3` - if (work.length === 16 && hash.length === 64) { - NanoPow.work_validate(work, hash, { difficulty }) - .then(result => { - validation.innerText = result - ? `\u2713` - : `\u274c` - }) - .catch(err => { - validation.innerText = `\u23f3` - }) - } + function startValidation(event) { + const difficulty = document.getElementById('difficulty')?.value + const work = document.getElementById('work')?.value + const hash = document.getElementById('hash')?.value + const validation = document.getElementById('validation') + const api = document.getElementById('api')?.value + const apiContainer = document.getElementById('api')?.parentElement + if (api === 'CPU') { + apiContainer.classList.add('warning') + apiContainer.title = 'Use only for testing with very low difficulty.' + } else { + apiContainer.classList.remove('warning') + apiContainer.title = '' } - document.getElementById('difficulty').addEventListener('input', startValidation) - document.getElementById('work').addEventListener('input', startValidation) - document.getElementById('hash').addEventListener('input', startValidation) - document.getElementById('api').addEventListener('input', startValidation) - - function startTest(event) { - event.target.disabled = true - const size = document.getElementById('size') - const difficulty = document.getElementById('difficulty') - const effort = document.getElementById('effort') - const api = document.getElementById('api') - const isOutputShown = document.getElementById('isOutputShown') - const isDebug = document.getElementById('isDebug') - const isSelfCheck = document.getElementById('isSelfCheck') - run(+size.value, difficulty.value, +effort.value, api.value, isOutputShown.checked, isDebug.checked, isSelfCheck.checked) - .then(() => { - event.target.disabled = false - isSelfCheck.checked = false + validation.innerText = `\u23f3` + if (work.length === 16 && hash.length === 64) { + NanoPow.work_validate(work, hash, { difficulty }) + .then(result => { + validation.innerText = result + ? `\u2713` + : `\u274c` + }) + .catch(err => { + validation.innerText = `\u23f3` }) } - - function startScore(event) { - event.target.disabled = true - const runs = document.getElementById('runs') - const size = document.getElementById('size') - const difficulty = document.getElementById('difficulty') - const effort = document.getElementById('effort') - const api = document.getElementById('api') - const isOutputShown = document.getElementById('isOutputShown') - score(+runs.value, +size.value, difficulty.value, +effort.value, api.value) - .then(() => event.target.disabled = false) - } - - document.getElementById('api_webgpu').disabled = navigator?.gpu == null - document.getElementById('api_webgpu').selected = navigator?.gpu != null - document.getElementById('api_webgl').disabled = glSize == null - document.getElementById('api_webgl').selected = navigator?.gpu == null && glSize != null - document.getElementById('api_wasm').selected = navigator?.gpu == null && glSize == null - document.getElementById('btnStartTest').addEventListener('click', startTest) - document.getElementById('btnStartScore').addEventListener('click', startScore) - document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency) / 2) - } catch (err) { - console.error(err) } + document.getElementById('difficulty').addEventListener('input', startValidation) + document.getElementById('work').addEventListener('input', startValidation) + document.getElementById('hash').addEventListener('input', startValidation) + document.getElementById('api').addEventListener('input', startValidation) + + function startTest(event) { + event.target.disabled = true + const api = document.getElementById('api') + const difficulty = document.getElementById('difficulty') + const effort = document.getElementById('effort') + const debug = document.getElementById('debug') + const size = document.getElementById('size') + const runs = document.getElementById('runs') + test(api.value, difficulty.value, +effort.value, debug.checked, +size.value, +runs.value) + .then(() => { + event.target.disabled = false + }) + } + + document.getElementById('api_webgpu').disabled = navigator?.gpu == null + document.getElementById('api_webgpu').selected = navigator?.gpu != null + document.getElementById('api_webgl').disabled = glSize == null + document.getElementById('api_webgl').selected = navigator?.gpu == null && glSize != null + document.getElementById('api_wasm').selected = navigator?.gpu == null && glSize == null + document.getElementById('btnStartTest').addEventListener('click', startTest) + document.getElementById('effort').value = Math.max(1, Math.floor(navigator.hardwareConcurrency) / 2)