Files
kefu/deploy/loading-performance-20260917/benchmark_protocol.py
T
2026-09-21 10:34:06 +08:00

113 lines
6.1 KiB
Python

"""Synthetic before/after benchmark; no live client, config, keys or conversations."""
from contextlib import closing
import gc
import hashlib
import importlib.util
import json
from pathlib import Path
import sqlite3
import statistics
import sys
import tempfile
import time
import tracemalloc
from unittest import mock
PROJECT=Path('C:/wechat_rpa')
sys.path.insert(0,str(PROJECT))
import wecom_native_sender as sender
import wxwork_db as current_db
old_path=PROJECT/'backups/loading-performance-20260917/wxwork_db.py'
spec=importlib.util.spec_from_file_location('wxwork_db_before_probe_optimization',old_path)
old_db=importlib.util.module_from_spec(spec)
spec.loader.exec_module(old_db)
def measured(callback):
gc.collect()
tracemalloc.start()
started=time.perf_counter()
result=callback()
elapsed=(time.perf_counter()-started)*1000
peak=tracemalloc.get_traced_memory()[1]
tracemalloc.stop()
return {'elapsedMs':round(elapsed,3),'pythonPeakBytes':peak,**result}
report={'synthetic':True,'liveProcessesInspected':False,'messagesSent':0,
'note':'Component benchmarks on deterministic synthetic files, not measured end-to-end production latency.'}
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary)
executable=root/'synthetic-WXWork.exe'
block=(b'synthetic build image\0'*(1048576//22+1))[:1048576]
with executable.open('wb') as handle:
for _ in range(64):handle.write(block)
size=executable.stat().st_size
def hash_before():
for _ in range(2):hashlib.sha256(executable.read_bytes()).hexdigest()
return {'hashReads':2,'bytesHashed':size*2}
def hash_after():
with mock.patch.object(sender,'_process_creation_id',return_value=123),sender.readonly_build_validation_scope() as stats:
for _ in range(2):sender._client_file_digest(executable,7,None,readonly=True)
return {'hashReads':stats['hashedFiles'],'bytesHashed':stats['bytesHashed'],'hashReuse':stats['reusedFiles']}
before=[];after=[]
for index in range(5):
if index%2:
after.append(measured(hash_after));before.append(measured(hash_before))
else:
before.append(measured(hash_before));after.append(measured(hash_after))
report['buildValidation']={'fileBytes':size,'beforeSamples':before,'afterSamples':after,
'beforeMedianMs':statistics.median(row['elapsedMs'] for row in before),
'afterMedianMs':statistics.median(row['elapsedMs'] for row in after),
'beforeMedianPeakBytes':statistics.median(row['pythonPeakBytes'] for row in before),
'afterMedianPeakBytes':statistics.median(row['pythonPeakBytes'] for row in after)}
source=root/'WXWork'
accounts=[str(100+index) for index in range(4)]
for account in accounts:
data=source/account/'Data';data.mkdir(parents=True)
for name in current_db._RELEVANT_DBS:
with closing(sqlite3.connect(data/name)) as connection:
if name=='message.db':
connection.execute('CREATE TABLE message_table(sender_id TEXT,conversation_id TEXT,content_type INT,send_time INT,content TEXT)')
connection.executemany('INSERT INTO message_table VALUES(?,?,?,?,?)',
(('200','S:'+account+'_200',2,index,'合成测试消息 '*8) for index in range(5000)))
elif name=='user.db':
connection.execute('CREATE TABLE user_table(id TEXT,name TEXT,real_name TEXT,account TEXT)')
connection.executemany('INSERT INTO user_table VALUES(?,?,?,?)',
((str(index),'合成客户'+str(index),'',account) for index in range(15000)))
elif name=='session.db':
connection.execute('CREATE TABLE conversation_table(id TEXT,name TEXT,roomname_remark TEXT,session_id TEXT)')
connection.executemany('INSERT INTO conversation_table VALUES(?,?,?,?)',
(('S:'+account+'_'+str(index),'合成会话'+str(index),'','') for index in range(3000)))
else:
connection.execute('CREATE TABLE synthetic_payload(payload BLOB)')
connection.execute('INSERT INTO synthetic_payload VALUES(zeroblob(262144))')
connection.commit()
def database_run(module,cache,scoped):
options={'account':'100','load_metadata':False} if scoped else {}
database=module.WXWorkDB(str(source),{},str(cache),**options)
try:
result={'currentAccountHealthy':database.health_check(account='100'),
'openedAccounts':len(database._conns),'loadedContacts':len(database.user_cache),
'preparedFiles':len(list(cache.rglob('*.db'))),
'preparedBytes':sum(path.stat().st_size for path in cache.rglob('*.db'))}
finally:database.close()
assert result['currentAccountHealthy']
return result
before=[];after=[]
for index in range(3):
before.append(measured(lambda:database_run(old_db,root/f'before-cache-{index}',False)))
after.append(measured(lambda:database_run(current_db,root/f'after-cache-{index}',True)))
report['databasePreparation']={'accounts':4,'sourceFiles':24,'contacts':60000,'sourceMessages':20000,
'beforeSamples':before,'afterSamples':after,
'beforeMedianMs':statistics.median(row['elapsedMs'] for row in before),
'afterMedianMs':statistics.median(row['elapsedMs'] for row in after),
'beforeMedianPeakBytes':statistics.median(row['pythonPeakBytes'] for row in before),
'afterMedianPeakBytes':statistics.median(row['pythonPeakBytes'] for row in after)}
output=Path(__file__).with_name('PROTOCOL_PERFORMANCE.json')
output.write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({'output':str(output),
'hashMs': [report['buildValidation']['beforeMedianMs'],report['buildValidation']['afterMedianMs']],
'databaseMs':[report['databasePreparation']['beforeMedianMs'],report['databasePreparation']['afterMedianMs']],
'preparedFiles':[before[0]['preparedFiles'],after[0]['preparedFiles']],
'contactsLoaded':[before[0]['loadedContacts'],after[0]['loadedContacts']]},ensure_ascii=False))