323 lines
8.7 KiB
Python
323 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
QwenPaw Startup Performance Analyzer
|
|
|
|
Collects startup performance data and outputs JSON for visualization.
|
|
"""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
|
|
def collect_import_time(output_dir):
|
|
"""Collect Python import time data."""
|
|
print("📊 Collecting import time...")
|
|
|
|
log_file = output_dir / "importtime.log"
|
|
|
|
cmd = [
|
|
sys.executable,
|
|
"-X",
|
|
"importtime",
|
|
"-m",
|
|
"qwenpaw",
|
|
"app",
|
|
"--help",
|
|
]
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
output = result.stderr + result.stdout
|
|
|
|
with open(log_file, "w", encoding="utf-8") as f:
|
|
f.write(output)
|
|
|
|
print(f" ✓ {log_file.name}")
|
|
return log_file
|
|
|
|
|
|
def collect_execution_trace(output_dir, script_dir):
|
|
"""Collect function execution trace using tracer module.
|
|
|
|
Args:
|
|
output_dir: Output directory for trace file
|
|
script_dir: Script directory containing tracer.py
|
|
|
|
Returns:
|
|
Path to trace file or None if failed
|
|
"""
|
|
print("🔍 Collecting execution trace...")
|
|
|
|
trace_file = output_dir / "execution_trace.json"
|
|
tracer_script = script_dir / "tracer.py"
|
|
|
|
if not tracer_script.exists():
|
|
print(f" ⚠ {tracer_script.name} not found")
|
|
return None
|
|
|
|
# Run the tracer script
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, str(tracer_script), str(trace_file)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
if trace_file.exists():
|
|
print(f" ✓ {trace_file.name}")
|
|
return trace_file
|
|
else:
|
|
print(" ⚠ Not generated")
|
|
return None
|
|
except Exception as e:
|
|
print(f" ⚠ Skipped: {e}")
|
|
return None
|
|
|
|
|
|
def parse_import_log(log_file):
|
|
"""Parse importtime log."""
|
|
print("🔬 Parsing import data...")
|
|
|
|
imports = []
|
|
|
|
with open(log_file, encoding="utf-8") as f:
|
|
for line in f:
|
|
match = re.search(
|
|
r"import time:\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(.+)$",
|
|
line,
|
|
)
|
|
if match:
|
|
imports.append(
|
|
{
|
|
"package": match.group(3).strip(),
|
|
"self_ms": int(match.group(1)) / 1000,
|
|
"cumulative_ms": int(match.group(2)) / 1000,
|
|
},
|
|
)
|
|
|
|
# Filter QwenPaw and third-party
|
|
qwenpaw = [i for i in imports if "qwenpaw" in i["package"]]
|
|
third_party = [
|
|
i
|
|
for i in imports
|
|
if not i["package"].startswith("_")
|
|
and "." not in i["package"]
|
|
and i["package"]
|
|
not in ["io", "os", "sys", "time", "re", "abc", "typing"]
|
|
]
|
|
|
|
pineagents.sort(key=lambda x: x["cumulative_ms"], reverse=True)
|
|
third_party.sort(key=lambda x: x["cumulative_ms"], reverse=True)
|
|
|
|
total_qwenpaw = sum(
|
|
i["cumulative_ms"] for i in qwenpaw if i["package"].count(".") == 1
|
|
)
|
|
total_third_party = sum(i["cumulative_ms"] for i in third_party[:10])
|
|
|
|
print(
|
|
f" ✓ {len(imports)} imports, {len(qwenpaw)} QwenPaw, {len(third_party)} third-party",
|
|
)
|
|
|
|
return {
|
|
"qwenpaw_imports": qwenpaw,
|
|
"third_party_imports": third_party,
|
|
"summary": {
|
|
"total_ms": total_qwenpaw + total_third_party,
|
|
"total_qwenpaw_ms": total_qwenpaw,
|
|
"total_third_party_ms": total_third_party,
|
|
},
|
|
}
|
|
|
|
|
|
def parse_execution_trace(trace_file):
|
|
"""Parse execution trace data."""
|
|
if not trace_file or not trace_file.exists():
|
|
return None
|
|
|
|
print("🔬 Parsing execution trace...")
|
|
|
|
try:
|
|
with open(trace_file, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
functions = [
|
|
{
|
|
"function": func,
|
|
"total_ms": sum(times) * 1000,
|
|
"count": len(times),
|
|
"avg_ms": sum(times) / len(times) * 1000,
|
|
}
|
|
for func, times in data["function_times"].items()
|
|
]
|
|
functions.sort(key=lambda x: x["total_ms"], reverse=True)
|
|
|
|
qwenpaw_funcs = [f for f in functions if "qwenpaw" in f["function"]]
|
|
|
|
print(f" ✓ {len(functions)} functions, {len(qwenpaw_funcs)} QwenPaw")
|
|
|
|
return {
|
|
"qwenpaw_functions": qwenpaw_funcs[:50],
|
|
"metadata": data["metadata"],
|
|
"execution_order": data["execution_order"],
|
|
}
|
|
except Exception as e:
|
|
print(f" ⚠ Parse failed: {e}")
|
|
return None
|
|
|
|
|
|
def main(): # pylint: disable=too-many-statements
|
|
"""Main function."""
|
|
print("=" * 60)
|
|
print("🐾 QwenPaw Startup Performance Analyzer")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
# Use __file__ to get absolute path
|
|
script_dir = Path(__file__).resolve().parent
|
|
output_dir = script_dir / "output"
|
|
output_dir.mkdir(exist_ok=True)
|
|
|
|
start = time.time()
|
|
|
|
# Collect data
|
|
log_file = collect_import_time(output_dir)
|
|
print()
|
|
trace_file = collect_execution_trace(output_dir, script_dir)
|
|
print()
|
|
|
|
# Parse data
|
|
import_data = parse_import_log(log_file)
|
|
print()
|
|
exec_data = parse_execution_trace(trace_file)
|
|
print()
|
|
|
|
# Generate report
|
|
print("💾 Saving analysis.json...")
|
|
|
|
report = {
|
|
"generated_at": datetime.now().isoformat(),
|
|
"import_analysis": import_data,
|
|
"execution_analysis": exec_data,
|
|
}
|
|
|
|
with open(
|
|
output_dir / "analysis.json",
|
|
"w",
|
|
encoding="utf-8",
|
|
) as f:
|
|
json.dump(report, f, indent=2, ensure_ascii=False)
|
|
|
|
print(" ✓ analysis.json")
|
|
print()
|
|
|
|
# Copy viewer
|
|
import shutil
|
|
|
|
html_src = script_dir / "viewer.html"
|
|
html_dst = output_dir / "report.html"
|
|
|
|
if html_src.exists():
|
|
shutil.copy(html_src, html_dst)
|
|
print(f"📊 Report: {html_dst}")
|
|
print()
|
|
|
|
# Start HTTP server and open browser
|
|
import threading
|
|
import http.server
|
|
import socketserver
|
|
import webbrowser
|
|
|
|
port = 8000
|
|
# Use absolute path from __file__
|
|
root_dir = Path(__file__).resolve().parent.parent.parent
|
|
|
|
# Calculate relative path from root to report
|
|
try:
|
|
relative_report = html_dst.relative_to(root_dir)
|
|
report_url = (
|
|
f"http://localhost:{port}/{relative_report.as_posix()}"
|
|
)
|
|
except ValueError:
|
|
# Fallback if paths are not relative
|
|
report_url = f"http://localhost:{port}/scripts/startup_profile/output/report.html"
|
|
|
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, directory=str(root_dir), **kwargs)
|
|
|
|
def log_message(self, fmt, *args):
|
|
pass # Suppress server logs
|
|
|
|
# Find available port
|
|
httpd = None
|
|
for attempt_port in range(port, port + 10):
|
|
try:
|
|
httpd = socketserver.TCPServer(("", attempt_port), Handler)
|
|
port = attempt_port
|
|
# Update URL with actual port
|
|
try:
|
|
relative_report = html_dst.relative_to(root_dir)
|
|
report_url = (
|
|
f"http://localhost:{port}/{relative_report.as_posix()}"
|
|
)
|
|
except ValueError:
|
|
report_url = (
|
|
f"http://localhost:{port}/"
|
|
"scripts/startup_profile/output/report.html"
|
|
)
|
|
break
|
|
except OSError:
|
|
continue
|
|
|
|
if httpd is None:
|
|
print(
|
|
"⚠ Could not find available port, please run manually:",
|
|
)
|
|
print(f" cd {root_dir}")
|
|
print(" python -m http.server 8000")
|
|
return
|
|
|
|
def run_server():
|
|
httpd.serve_forever()
|
|
|
|
server_thread = threading.Thread(target=run_server, daemon=True)
|
|
server_thread.start()
|
|
|
|
print(
|
|
f"🌐 Server started at http://localhost:{port}/ (root: {root_dir.name})",
|
|
)
|
|
print(f"📊 Opening: {report_url}")
|
|
print()
|
|
print("Press Ctrl+C to stop the server")
|
|
print()
|
|
|
|
webbrowser.open(report_url)
|
|
|
|
try:
|
|
# Keep main thread alive
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print("\n👋 Server stopped")
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print(f"✅ Done in {time.time() - start:.1f}s")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|