Files
SystemSimulationApp/tests/manual/profile_pipe_iterations.py
T
lujingze 3bc4be3c06 优化原生结果编码传输与浏览器缓存,记录八路性能基线
原生结果series通过字节索引直传,C端使用Ryu精确回读编码和64 KiB批量写出;网页采用Float64缓存和CSV工作线程,减少结果处理与保存等待。

补充八路AME曲线核查、全流程分阶段计时、独立编码基准和复现工具,固定后续优化采用修正八路及rtol=1e-8。C写出1.1808→0.1638 s,点击到可查看8.0100→6.9756 s。

验证:最终10项编码专项、29项相关后端回归通过;8份原生结果逐位一致,16次网页结果/CSV/刷新恢复通过。前端构建及缓存/CSV专项在本轮结果处理工作中通过。环境、原始大结果与临时构建不纳入Git。
2026-09-11 15:09:15 +00:00

441 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Instrument isolated pipe solvers; these runs are diagnostics, never benchmarks.
Prepare: python tests/manual/profile_pipe_iterations.py --output-dir test/pipe-profile --prepare-only
Run the prepared diagnostic programs: use the same command without --prepare-only.
All mutations except this test helper stay below the ignored output directory.
The replay contains EVERY resistance-law input from the guarded solver's RHS
trajectory, including its scalar low-Re analytic branch. Cache hits, zero dp,
and the separate PNL00R analytic law are counted but do not enter this replay.
"""
from __future__ import annotations
import argparse
from hashlib import sha256
import json
import os
from pathlib import Path
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from app.main import compile_system_xml_network
from app.simulation.backends import simulation_config
from app.simulation.native_codegen import build as builder
from app.simulation.native_codegen.compiler import compile_native_program
from app.simulation.native_codegen.input import load_input
VARIANTS = ('guarded-newton', 'previous-newton', 'fixed-point')
FIELDS = '''cache_requests cache_hits cache_misses flow_calls zero_pressure_calls
pnl00r_analytic_calls resistance_calls scalar_analytic_calls iterative_calls
iterations_total iterations_max reached_last_iteration exhausted_limit
algorithm_converged failed_before_iteration finite_returns nonfinite_returns
bisections residual_pass residual_fail residual_nonfinite
residual_fail_after_algorithm_converged residual_pass_after_limit
invalid_inputs upper_bracket_evaluations upper_bracket_exhausted
bisection_after_poor_progress bisection_invalid_or_outside float_stagnation
'''.split()
PROFILE_PREFIX = r'''
/* Test-only instrumentation, injected in an isolated source tree. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define PROFILE_VARIANT "@VARIANT@"
#define PROFILE_FIXED @FIXED@
#define PROFILE_LIMIT(kind) @LIMIT@
#define PROFILE_FIELDS(X) @FIELDS@
typedef struct {
#define PROFILE_DECLARE(name) unsigned long long name;
PROFILE_FIELDS(PROFILE_DECLARE)
#undef PROFILE_DECLARE
unsigned long long histogram[129];
double maximum_relative_residual;
} PipeProfile;
static PipeProfile profile_stats[2][4];
static unsigned long long profile_rhs_calls,profile_capture_records;
static int profile_in_rhs,profile_kind,profile_last_exhausted;
static FILE *profile_capture;
static PipeProfile *profile_bucket(int kind) {
if(kind<0 || kind>3){fprintf(stderr,"Unexpected pipe kind %d\n",kind);exit(71);}
return &profile_stats[profile_in_rhs?0:1][kind];
}
void pipe_profile_rhs_enter(void){profile_in_rhs=1;profile_rhs_calls++;}
void pipe_profile_rhs_leave(void){profile_in_rhs=0;}
static void profile_add(PipeProfile *total,const PipeProfile *value) {
#define PROFILE_ADD(name) total->name+=value->name;
PROFILE_FIELDS(PROFILE_ADD)
#undef PROFILE_ADD
if(value->iterations_max>total->iterations_max)total->iterations_max=value->iterations_max;
for(int i=0;i<129;i++)total->histogram[i]+=value->histogram[i];
if(value->maximum_relative_residual>total->maximum_relative_residual)
total->maximum_relative_residual=value->maximum_relative_residual;
}
static void profile_write_bucket(FILE *f,const PipeProfile *value) {
fprintf(f,"{");
#define PROFILE_WRITE(name) fprintf(f,"\"" #name "\":%llu,",value->name);
PROFILE_FIELDS(PROFILE_WRITE)
#undef PROFILE_WRITE
fprintf(f,"\"maximum_relative_residual\":%.17g,\"iteration_histogram\":{",value->maximum_relative_residual);
int comma=0;
for(int i=0;i<129;i++)if(value->histogram[i]) {
fprintf(f,"%s\"%d\":%llu",comma?",":"",i,value->histogram[i]);comma=1;
}
fprintf(f,"}}");
}
static void profile_dump(void) {
const char *path=getenv("PIPE_PROFILE_JSON");
if(profile_capture){if(fclose(profile_capture))exit(73);profile_capture=NULL;}
if(!path)return;
FILE *f=fopen(path,"wb");if(!f){perror(path);exit(73);}
fprintf(f,"{\"variant\":\"%s\",\"rhs_calls\":%llu,\"capture_records\":%llu,",PROFILE_VARIANT,profile_rhs_calls,profile_capture_records);
for(int scope=0;scope<2;scope++) {
PipeProfile total={0};
for(int kind=0;kind<4;kind++)profile_add(&total,&profile_stats[scope][kind]);
/* iterations_max is a maximum, unlike the additive counters. */
total.iterations_max=0;
for(int kind=0;kind<4;kind++)if(profile_stats[scope][kind].iterations_max>total.iterations_max)
total.iterations_max=profile_stats[scope][kind].iterations_max;
fprintf(f,"%s\"%s\":{\"total\":",scope?",":"",scope?"non_rhs":"rhs");
profile_write_bucket(f,&total);fprintf(f,",\"by_kind\":{");
for(int kind=0;kind<4;kind++) {
fprintf(f,"%s\"%d\":",kind?",":"",kind);profile_write_bucket(f,&profile_stats[scope][kind]);
}
fprintf(f,"}}");
}
fprintf(f,"}\n");if(fclose(f))exit(73);
}
void pipe_profile_install(void) {
const char *path=getenv("PIPE_PROFILE_CAPTURE");
if(path){profile_capture=fopen(path,"wb");if(!profile_capture){perror(path);exit(73);}}
if(atexit(profile_dump)){fprintf(stderr,"Cannot register profile writer\n");exit(73);}
}
static void profile_save_input(double base,double d,double length,double rr,double den,int kind) {
if(profile_in_rhs && profile_capture) {
/* Six IEEE doubles, native endian; no sampling or deduplication. */
double input[]={base,d,length,rr,den,(double)kind};
if(fwrite(input,sizeof(input),1,profile_capture)!=1){perror("capture");exit(73);}
profile_capture_records++;
}
}
'''
PROFILE_SOLVE = r'''
/* Independently factored Darcy law: no production slope or solver status is
consulted. Long double reduces rounding noise in the returned-q residual. */
static long double profile_reference_friction(long double re,long double rr) {
if(!(re>0))return NAN;
long double laminar=64/re;
if(re<=89.96829989L)return laminar;
long double smooth=powl(-1.8L*log10l(6.9L/re),-2),turbulent=smooth;
if(rr>0) {
long double fully_rough=powl(-2*log10l(rr/3.7L),-2);
long double weight=1/(1+powl(180/(re*rr),2));
turbulent=(1-weight)*smooth+weight*fully_rough;
}
long double blend=powl((re-89.96829989L)/2741.96700831L,8.37293695L);
return (laminar+blend*turbulent)/(1+blend);
}
static void profile_returned_residual(PipeProfile *s,double q,double den,double K,double rr,
int converged,int exhausted) {
if(!isfinite(q)){s->nonfinite_returns++;s->residual_nonfinite++;return;}
s->finite_returns++;
long double re=fabsl((long double)q/(den/4)),residual;
if(K==0 && q==0)residual=0;
else residual=fabsl(re*re*profile_reference_friction(re,rr)/K-1);
if(!isfinite(residual)){s->residual_nonfinite++;return;}
if(residual>s->maximum_relative_residual)s->maximum_relative_residual=(double)residual;
if(residual<=1e-9L){s->residual_pass++;if(exhausted)s->residual_pass_after_limit++;}
else {s->residual_fail++;if(converged)s->residual_fail_after_algorithm_converged++;}
}
static double profile_solve(double base,double d,double length,double rr,double den,int kind) {
PipeProfile *s=profile_bucket(kind);
double K=pow(4*base/den,2)*d/length;
profile_save_input(base,d,length,rr,den,kind);
s->resistance_calls++;
if(!(K>=0 && rr>=0 && den/4>0) || !isfinite(K) || !isfinite(rr) || !isfinite(den/4))s->invalid_inputs++;
int iterations=0,converged=0,bisections=0,exhausted=0;
double q;
profile_kind=kind;profile_last_exhausted=0;
#if PROFILE_FIXED
double rough_limit=pipe_rough_limit(rr);
q=sqrt(d/(length*.02))*base;
for(int i=0;i<PROFILE_LIMIT(kind);i++) {
iterations=i+1;
double next=sqrt(d/(length*pipe_friction_prepared(4*fabs(q)/den,rr,rough_limit)))*base;
if(fabs(next-q)<=fmax(1e-12,fabs(q)*1e-9)){q=next;converged=1;break;}
q=.5*(q+next);
}
exhausted=!converged;
#else
NativePipeSolve status;
q=native_pipe_resistance(K,rr,den/4,&status)*den/4;
iterations=status.iterations;converged=status.converged;bisections=status.bisections;
exhausted=profile_last_exhausted;
#endif
if(iterations<0 || iterations>128){fprintf(stderr,"Unexpected iteration count\n");exit(71);}
s->histogram[iterations]++;s->iterations_total+=(unsigned)iterations;s->bisections+=(unsigned)bisections;
if((unsigned)iterations>s->iterations_max)s->iterations_max=(unsigned)iterations;
if(iterations)s->iterative_calls++;
else if(converged)s->scalar_analytic_calls++;
else s->failed_before_iteration++;
if(iterations==PROFILE_LIMIT(kind))s->reached_last_iteration++;
if(exhausted)s->exhausted_limit++;
if(converged)s->algorithm_converged++;
profile_returned_residual(s,q,den,K,rr,converged,exhausted);
return q;
}
'''
REPLAY_MAIN = r'''
#include "native/components/kernels.c"
int main(int argc,char **argv) {
if(argc!=2)return 64;
pipe_profile_install();profile_in_rhs=1;
FILE *f=fopen(argv[1],"rb");if(!f){perror(argv[1]);return 73;}
double input[6];size_t count;
while((count=fread(input,1,sizeof(input),f))==sizeof(input)) {
profile_solve(input[0],input[1],input[2],input[3],input[4],(int)input[5]);
}
int failed=count || ferror(f);fclose(f);return failed?74:0;
}
'''
def replace_once(source: str, old: str, new: str) -> str:
if source.count(old) != 1:
raise ValueError(f'Expected exactly one audited source fragment: {old[:100]!r}')
return source.replace(old, new, 1)
def instrument(source: str, variant: str) -> str:
fixed = variant == 'fixed-point'
limit = '(kind==0?64:16)' if fixed else '80' if variant == 'previous-newton' else '128'
prefix = PROFILE_PREFIX.replace('@VARIANT@', variant).replace('@FIXED@', str(int(fixed)))
prefix = prefix.replace('@LIMIT@', limit).replace('@FIELDS@', ' '.join(f'X({name})' for name in FIELDS))
source = replace_once(source, '#include <stddef.h>', '#include <stddef.h>\n' + prefix)
# Exhaustion is marked at the actual loop fall-through, not inferred from
# visiting the last allowed iteration (which can still converge).
start = source.index('double native_pipe_resistance(')
end = source.index('double native_pipe_flow(', start)
resistance = source[start:end]
ending = ' return NAN;\n}\n'
if not resistance.endswith(ending):
raise ValueError('Unexpected resistance function ending')
resistance = resistance[:-len(ending)] + ' profile_last_exhausted=1;return NAN;\n}\n'
if variant == 'guarded-newton':
resistance = replace_once(resistance, ' double value=hi*hi*pipe_friction_prepared(hi,rr,rough);',
' profile_bucket(profile_kind)->upper_bracket_evaluations++;\n double value=hi*hi*pipe_friction_prepared(hi,rr,rough);')
resistance = replace_once(resistance, ' if(!bracketed)return NAN;',
' if(!bracketed){profile_bucket(profile_kind)->upper_bracket_exhausted++;return NAN;}')
resistance = replace_once(resistance, ' if(bisect) {',
''' if(bisect) {
if(previous_newton && fabs(F)>.5*previous_residual)profile_bucket(profile_kind)->bisection_after_poor_progress++;
if(!(slope>0) || !isfinite(slope) || !isfinite(next) || next<=lo || next>=hi)
profile_bucket(profile_kind)->bisection_invalid_or_outside++;''')
resistance = replace_once(resistance, ' if(next<=lo || next>=hi) {',
' if(next<=lo || next>=hi) {\n profile_bucket(profile_kind)->float_stagnation++;')
elif variant == 'previous-newton':
# Count each evaluation of the original loop condition without changing
# its short-circuit behaviour or the original upper endpoint arithmetic.
resistance = replace_once(resistance, 'i<128 && hi*hi*pipe_friction_prepared(hi,rr,rough)<K',
'i<128 && (profile_bucket(profile_kind)->upper_bracket_evaluations++,hi*hi*pipe_friction_prepared(hi,rr,rough)<K)')
resistance = replace_once(resistance, 'status->bisections++;',
'status->bisections++;profile_bucket(profile_kind)->bisection_invalid_or_outside++;')
source = source[:start] + resistance + PROFILE_SOLVE + source[end:]
source = replace_once(source, ' if(fabs(p1-p2)<=1e-8) return 0;',
''' PipeProfile *profile=profile_bucket(kind);profile->flow_calls++;
if(fabs(p1-p2)<=1e-8){profile->zero_pressure_calls++;return 0;}''')
source = replace_once(source, ' if(4*lam/den<=1000) return sign*lam;',
' if(4*lam/den<=1000){profile->pnl00r_analytic_calls++;return sign*lam;}')
source = replace_once(source,
' double base=area*p*cm/sqrt(T),K=pow(4*base/den,2)*d/length;\n return sign*native_pipe_resistance(K,rr,den/4,NULL)*den/4;',
' double base=area*p*cm/sqrt(T);\n return sign*profile_solve(base,d,length,rr,den,kind);')
source = replace_once(source, ' if(cache->valid && cache->p1==p1 && cache->p2==p2 && cache->T==T &&',
' PipeProfile *profile=profile_bucket(kind);profile->cache_requests++;\n if(cache->valid && cache->p1==p1 && cache->p2==p2 && cache->T==T &&')
source = replace_once(source, ' return cache->flow;\n double result=properties?',
' {profile->cache_hits++;return cache->flow;}\n profile->cache_misses++;\n double result=properties?')
return source
def git(*arguments: str) -> str:
return subprocess.check_output(['git', *arguments], cwd=ROOT, text=True).strip()
def prepare(args, out: Path) -> dict:
if (out / 'prepared.json').exists():
metadata = json.loads((out / 'prepared.json').read_text())
if metadata['input_sha256'] != sha256(args.input.read_bytes()).hexdigest():
raise ValueError('Prepared model no longer matches the input file')
if metadata['script_sha256'] != sha256(Path(__file__).read_bytes()).hexdigest():
raise ValueError('Diagnostic helper changed; choose a fresh output directory')
return metadata
previous = git('rev-parse', args.previous_ref)
current = git('rev-parse', args.current_ref)
xml, doc = load_input(args.input)
config = simulation_config(doc.simulation)
if config.rtol != 1e-8 or config.t_start != 0 or config.t_stop != 10:
raise ValueError('This experiment requires API default rtol=1e-8 and 0–10 s model settings')
program = compile_native_program(compile_system_xml_network(doc))
out.mkdir(parents=True, exist_ok=True)
(out / 'input.xml').write_bytes(xml)
(out / 'input.json').write_bytes(args.input.read_bytes())
paths = git('ls-tree', '-r', '--name-only', current, 'native').splitlines()
metadata = dict(input=str(args.input.resolve()), input_sha256=sha256(args.input.read_bytes()).hexdigest(),
xml_sha256=sha256(xml).hexdigest(), script_sha256=sha256(Path(__file__).read_bytes()).hexdigest(),
previous_revision=previous, current_revision=current, settings=vars(config),
sample_step=doc.simulation.sample_step, variants={},
measurement_note='Instrumented times are diagnostic overhead and MUST NOT be used as production benchmark results.',
counter_scope='rhs counts native_rhs/model_eval calls including rejected trials and Jacobian differences; non_rhs includes initialization/output/probe evaluations.',
replay_scope='All guarded RHS resistance calls, including scalar analytic low-Re cases; no sampling or deduplication. Cache hits, zero pressure difference and direct PNL00R analytic calls are excluded and counted separately.',
capture_format='Native-endian IEEE-754 binary64 records: base, diameter, length, relative roughness, den=pi*d*mu, kind (six doubles, 48 bytes). Replay on the same host.',
residual_test='At actual returned q, independently factored long-double f(Re) evaluates abs(Re^2*f(Re)/K-1)<=1e-9. Separate from each algorithm stopping rule.')
original_native = builder.NATIVE
try:
for variant in VARIANTS:
directory = out / variant
revision = current if variant == 'guarded-newton' else previous
for name in paths:
target = directory / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(subprocess.check_output(['git', 'show', f'{revision}:{name}'], cwd=ROOT))
kernel = directory / 'native/components/kernels.c'
original_hash = sha256(kernel.read_bytes()).hexdigest()
kernel.write_text(instrument(kernel.read_text(), variant))
common = directory / 'native/runtime/common.c'
common.write_text(replace_once(common.read_text(), ' return model_eval(t,y,dy,w);',
''' extern void pipe_profile_rhs_enter(void),pipe_profile_rhs_leave(void);
pipe_profile_rhs_enter();int ok=model_eval(t,y,dy,w);pipe_profile_rhs_leave();return ok;'''))
main = directory / 'native/runtime/main.c'
main.write_text(replace_once(main.read_text(), 'int main(int argc, char **argv) {',
'int main(int argc, char **argv) {\n extern void pipe_profile_install(void);pipe_profile_install();'))
builder.NATIVE = directory / 'native'
build = builder.build_native(program, cache_dir=out / 'cache')
replay_source = directory / 'replay.c'
replay_source.write_text(REPLAY_MAIN)
replay = directory / ('replay.exe' if os.name == 'nt' else 'replay')
compiler, _, _ = builder.toolchain()
command = [compiler, '-std=c11', '-O3', '-Wall', '-Wextra', '-Werror', '-ffp-contract=off',
'-fno-fast-math', '-I', str(directory / 'native/include'), str(replay_source), '-lm', '-o', str(replay)]
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
(directory / 'replay-build.log').write_text(compiled.stdout + compiled.stderr)
if compiled.returncode:
raise RuntimeError(f'Replay compilation failed: {compiled.stderr}')
metadata['variants'][variant] = dict(executable=str(build.executable), replay=str(replay),
build_key=build.manifest['buildKey'], original_kernel_sha256=original_hash,
instrumented_kernel_sha256=sha256(kernel.read_bytes()).hexdigest())
print(f'Prepared {variant}', flush=True)
finally:
builder.NATIVE = original_native
(out / 'prepared.json').write_text(json.dumps(metadata, ensure_ascii=False, indent=2) + '\n')
return metadata
def load_stats(path: Path) -> dict:
stats = json.loads(path.read_text())
for scope in ('rhs', 'non_rhs'):
for row in [stats[scope]['total'], *stats[scope]['by_kind'].values()]:
assert row['cache_requests'] == row['cache_hits'] + row['cache_misses']
assert row['flow_calls'] == row['zero_pressure_calls'] + row['pnl00r_analytic_calls'] + row['resistance_calls'] or not row['flow_calls']
assert row['resistance_calls'] == row['iterative_calls'] + row['scalar_analytic_calls'] + row['failed_before_iteration']
assert row['resistance_calls'] == sum(row['iteration_histogram'].values())
assert row['iterations_total'] == sum(int(k) * v for k, v in row['iteration_histogram'].items())
assert row['resistance_calls'] == row['finite_returns'] + row['nonfinite_returns']
assert row['resistance_calls'] == row['residual_pass'] + row['residual_fail'] + row['residual_nonfinite']
assert row['exhausted_limit'] <= row['reached_last_iteration']
return stats
def run(metadata: dict, out: Path, timeout: float):
summary_path = out / 'summary.json'
if summary_path.exists():
raise ValueError('Diagnostic results already exist; choose a fresh output directory')
settings = metadata['settings']
capture = out / 'guarded-rhs-resistance-inputs.bin'
rows = {}
for variant in VARIANTS:
directory = out / variant / 'trajectory'
directory.mkdir(parents=True, exist_ok=False)
result_path = directory / 'result.json'
stats_path = directory / 'profile.json'
env = os.environ.copy()
env.pop('PIPE_PROFILE_CAPTURE', None)
env['PIPE_PROFILE_JSON'] = str(stats_path)
if variant == 'guarded-newton':
env['PIPE_PROFILE_CAPTURE'] = str(capture)
executable = metadata['variants'][variant]['executable']
command = [executable, '--method', settings['method'], '--start', str(settings['t_start']),
'--stop', str(settings['t_stop']), '--sample-step', str(metadata['sample_step']),
'--max-step', str(settings['max_step']), '--rtol', str(settings['rtol']),
'--timeout', str(timeout), '--output', str(result_path)]
print(f'Starting diagnostic trajectory: {variant}', flush=True)
with (directory / 'worker.log').open('w') as log:
try:
process = subprocess.run(command, cwd=Path(executable).parent, env=env,
stdout=subprocess.DEVNULL, stderr=log, timeout=timeout+15)
exit_code = process.returncode
except subprocess.TimeoutExpired:
exit_code = 'external-timeout'
data = json.loads(result_path.read_text()) if result_path.exists() else {}
# Deliberately omit measured times from the cross-variant summary.
trajectory = {key:data.get(key) for key in ('success', 'status', 'message', 'simulatedUntil',
'nfev', 'acceptedSteps', 'rejectedSteps', 'njev', 'nlu', 'stateTransitions')}
trajectory['exit_code'] = exit_code
rows[variant] = dict(trajectory=trajectory, profile=load_stats(stats_path) if stats_path.exists() else None)
print(json.dumps(dict(variant=variant, **trajectory), ensure_ascii=False), flush=True)
if capture.stat().st_size % 48:
raise ValueError('Truncated replay capture')
count = capture.stat().st_size // 48
guarded = rows['guarded-newton']['profile']
if guarded is None or count != guarded['rhs']['total']['resistance_calls'] or count != guarded['capture_records']:
raise ValueError('Capture does not contain every guarded RHS resistance call')
for variant in VARIANTS:
directory = out / variant / 'replay-results'
directory.mkdir(exist_ok=False)
stats_path = directory / 'profile.json'
env = os.environ.copy()
env.pop('PIPE_PROFILE_CAPTURE', None)
env['PIPE_PROFILE_JSON'] = str(stats_path)
print(f'Replaying all {count} common inputs: {variant}', flush=True)
with (directory / 'worker.log').open('w') as log:
subprocess.run([metadata['variants'][variant]['replay'], str(capture)], env=env,
stdout=subprocess.DEVNULL, stderr=log, timeout=300, check=True)
replay = load_stats(stats_path)
if replay['rhs']['total']['resistance_calls'] != count:
raise ValueError('Replay count mismatch')
rows[variant]['replay'] = replay
# Identical input replay must reproduce every resistance-solve counter for
# the guarded solver, excluding cache/flow bookkeeping performed upstream.
exempt = {'cache_requests', 'cache_hits', 'cache_misses', 'flow_calls', 'zero_pressure_calls', 'pnl00r_analytic_calls'}
for kind in ('total', '0', '1', '2', '3'):
actual = guarded['rhs']['total'] if kind == 'total' else guarded['rhs']['by_kind'][kind]
replay = rows['guarded-newton']['replay']['rhs']['total'] if kind == 'total' else rows['guarded-newton']['replay']['rhs']['by_kind'][kind]
assert {k:v for k,v in actual.items() if k not in exempt} == {k:v for k,v in replay.items() if k not in exempt}
summary = dict(metadata=metadata, capture=dict(path=str(capture), records=count, bytes=capture.stat().st_size,
sha256=sha256(capture.read_bytes()).hexdigest(), sampling='none'), variants=rows)
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + '\n')
print(f'Diagnostic profile complete: {summary_path}', flush=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--input', type=Path, default=ROOT / 'tests/data/test-mql-8-corrected.json')
parser.add_argument('--output-dir', type=Path, required=True)
parser.add_argument('--previous-ref', default='5d5a2e1')
parser.add_argument('--current-ref', default='808c484')
parser.add_argument('--timeout', type=float, default=120)
parser.add_argument('--prepare-only', action='store_true')
args = parser.parse_args()
out = args.output_dir.resolve()
# Keep diagnostic native copies out of production sources and tracked data.
if not out.is_relative_to(ROOT / 'test'):
parser.error('--output-dir must be below the ignored repository test/ directory')
metadata = prepare(args, out)
if not args.prepare_only:
run(metadata, out, args.timeout)
if __name__ == '__main__':
main()