优化原生结果编码传输与浏览器缓存,记录八路性能基线
原生结果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。
This commit is contained in:
1 parent
808c484f5b
commit
3bc4be3c06
55 files changed
+20522
-141
No files matched your search
@@ -0,0 +1,143 @@
|
||||
"""The HTTP fast path must preserve real native values and task semantics."""
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import asyncio
|
||||
from app.main import app, _register_simulation_task, _request_simulation_task_cancel, simulation_event_stream
|
||||
from app.simulation.backends import simulation_config
|
||||
from app.simulation.native_codegen.build import build_native
|
||||
from app.simulation.native_codegen.compiler import compile_native_program
|
||||
from app.simulation.native_codegen.input import load_input
|
||||
from app.simulation.native_codegen.runner import execute_native
|
||||
from app.simulation.native_codegen.transport import NativeSeriesJson, read_indexed_result, serialize_result_parts
|
||||
from app.main import compile_system_xml_network
|
||||
|
||||
|
||||
class AsgiClient:
|
||||
"""Exercise real routing/response bodies without an optional HTTP client dependency."""
|
||||
def __init__(self, application): self.application = application
|
||||
def post(self, path, *, content=b'', headers=None): return self.request('POST', path, content, headers)
|
||||
def get(self, path): return self.request('GET', path, b'', None)
|
||||
def request(self, method, path, content, headers):
|
||||
async def run():
|
||||
messages = []
|
||||
scope = {'type':'http', 'asgi':{'version':'3.0','spec_version':'2.4'},
|
||||
'http_version':'1.1','method':method,'scheme':'http','path':path,
|
||||
'raw_path':path.encode(),'query_string':b'', 'root_path':'',
|
||||
'headers':[(k.lower().encode(),v.encode()) for k,v in (headers or {}).items()],
|
||||
'server':('testserver',80),'client':('127.0.0.1',1234)}
|
||||
async def receive(): return {'type':'http.request','body':content,'more_body':False}
|
||||
async def send(message): messages.append(message)
|
||||
await self.application(scope,receive,send)
|
||||
status = next(m['status'] for m in messages if m['type']=='http.response.start')
|
||||
body = b''.join(m.get('body',b'') for m in messages if m['type']=='http.response.body')
|
||||
return SimpleNamespace(status_code=status,content=body,json=lambda:json.loads(body))
|
||||
return asyncio.run(run())
|
||||
|
||||
|
||||
class NativeResultTransportTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix='test-native-transport-')
|
||||
cls.root = Path(cls.temp.name)
|
||||
cls.xml = Path('tests/fixtures/native-skill-test.xml').read_bytes().replace(b'tStop="10"', b'tStop="0.1"')
|
||||
source = cls.root/'input.xml'; source.write_bytes(cls.xml)
|
||||
_, document = load_input(source)
|
||||
cls.config = simulation_config(document.simulation)
|
||||
cls.build = build_native(compile_native_program(compile_system_xml_network(document)))
|
||||
cls.normal = execute_native(cls.build, cls.config, .001, run_dir=cls.root/'ordinary')
|
||||
cls.indexed = execute_native(cls.build, cls.config, .001, run_dir=cls.root/'indexed', raw_series=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.temp.cleanup()
|
||||
|
||||
def test_real_native_series_are_equal_without_large_python_parse(self):
|
||||
self.assertIsInstance(self.indexed['series'], NativeSeriesJson)
|
||||
self.assertEqual(self.indexed['series'].materialize(), self.normal['series'])
|
||||
self.assertEqual(self.indexed['series'].sample_count, len(self.normal['series']['time']))
|
||||
for key in ('final', 'finalState', 'nfev', 'acceptedSteps', 'rejectedSteps'):
|
||||
self.assertEqual(self.indexed[key], self.normal[key])
|
||||
self.assertGreater(len(self.indexed['series'].data), 100000)
|
||||
original = json.loads
|
||||
sizes = []
|
||||
def small_only(value):
|
||||
sizes.append(len(value))
|
||||
self.assertLess(len(value), 50000, 'Raw series was decoded through Python')
|
||||
return original(value)
|
||||
with patch('app.simulation.native_codegen.transport.json', SimpleNamespace(loads=small_only)):
|
||||
payload = read_indexed_result(self.root/'indexed/result.json', self.root/'indexed/result-index.json')
|
||||
self.assertEqual(len(sizes), 2) # index and small metadata only
|
||||
self.assertEqual(payload['series'].data, self.indexed['series'].data)
|
||||
|
||||
def test_real_http_stream_and_retained_task_get_keep_same_schema(self):
|
||||
client = AsgiClient(app)
|
||||
ident = 'transport-'+uuid4().hex
|
||||
response = client.post('/api/system-xml/simulate-stream', content=self.xml, headers={'X-Simulation-Id':ident})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
events = [json.loads(line) for line in response.content.splitlines()]
|
||||
result = next(event['result'] for event in events if event['event'] == 'result')
|
||||
self.assertTrue(result['success'])
|
||||
self.assertEqual(result['simulatedUntil'], .1)
|
||||
self.assertEqual(result['diagnostics']['sampleCount'], len(result['series']['time']))
|
||||
# The bytes survive deletion of the worker directory and repeated task reads.
|
||||
for _ in range(2):
|
||||
retained = client.get('/api/system-xml/simulations/'+ident)
|
||||
self.assertEqual(retained.status_code, 200)
|
||||
self.assertEqual(retained.json()['result'], result)
|
||||
synchronous = client.post('/api/system-xml/simulate', content=self.xml).json()
|
||||
for key in ('series', 'final', 'variables', 'model', 'simulation'):
|
||||
self.assertEqual(synchronous[key], result[key])
|
||||
|
||||
def test_cancelled_raw_stream_keeps_partial_result_and_public_status(self):
|
||||
for reason, status in [('user', 'stopped'), ('stalled', 'stalled')]:
|
||||
task = _register_simulation_task('transport-'+uuid4().hex)
|
||||
_request_simulation_task_cancel(task, reason)
|
||||
body = b''.join(part.encode() if isinstance(part, str) else part
|
||||
for part in simulation_event_stream(self.xml, task=task, raw_series=True))
|
||||
result = next(event['result'] for event in map(json.loads, body.splitlines()) if event['event']=='result')
|
||||
self.assertEqual(result['status'], status)
|
||||
self.assertTrue(result['partial'])
|
||||
self.assertLess(result['simulatedUntil'], .1)
|
||||
self.assertEqual(result['series']['time'][-1], result['simulatedUntil'])
|
||||
self.assertEqual(AsgiClient(app).get('/api/system-xml/simulations/'+task.simulation_id).json()['result'], result)
|
||||
|
||||
def test_index_corruption_or_truncated_output_is_rejected(self):
|
||||
directory = self.root/'corrupt'; directory.mkdir(exist_ok=True)
|
||||
data = (self.root/'indexed/result.json').read_bytes()
|
||||
original = json.loads((self.root/'indexed/result-index.json').read_bytes())
|
||||
output = directory/'result.json'; index = directory/'index.json'
|
||||
output.write_bytes(data)
|
||||
for change in ({'version':2}, {'version':True}, {'version':1.0}, {'seriesStart':True}, {'seriesStart':-1}, {'seriesEnd':len(data)+1},
|
||||
{'resultBytes':len(data)-1}, {'sampleCount':-1}, {'seriesStart':original['seriesStart']+1}):
|
||||
with self.subTest(change=change):
|
||||
index.write_text(json.dumps(original | change))
|
||||
with self.assertRaises(ValueError): read_indexed_result(output,index)
|
||||
index.write_text(json.dumps(original)); output.write_bytes(data[:-2])
|
||||
with self.assertRaises(ValueError): read_indexed_result(output,index)
|
||||
|
||||
def test_json_framing_and_escaping_cannot_confuse_raw_series(self):
|
||||
values = {'time':[0,1], 'odd\\"},"final":{\n温度':[1e-300,-0.0]}
|
||||
raw = NativeSeriesJson(json.dumps(values,ensure_ascii=False).encode(),2)
|
||||
for payload in ({'result':{'series':raw}},
|
||||
{'event':'result','message':'\n"series":{},"result":null',
|
||||
'result':{'series':raw,'label':'\\"雪\n','success':True}}):
|
||||
encoded = b''.join(serialize_result_parts(payload))
|
||||
expected = dict(payload, result=dict(payload['result'], series=values))
|
||||
self.assertEqual(json.loads(encoded), expected)
|
||||
self.assertEqual(json.loads(b''.join(serialize_result_parts({'event':'progress'}))), {'event':'progress'})
|
||||
|
||||
def test_solve_only_raw_series_is_empty_object(self):
|
||||
result = execute_native(self.build, replace(self.config,t_stop=.001),.001,
|
||||
run_dir=self.root/'solve-only', raw_series=True,record_samples=False)
|
||||
self.assertEqual(result['series'].data,b'{}')
|
||||
self.assertEqual(result['series'].sample_count,0)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Reference in new issue
Block a user