PraisonAI 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
praisonai/profiler.py
ADDED
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PraisonAI Profiler Module
|
|
3
|
+
|
|
4
|
+
Standardized profiling for performance monitoring across praisonai and praisonai-agents.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Import timing
|
|
8
|
+
- Function execution timing
|
|
9
|
+
- Flow tracking
|
|
10
|
+
- File/module usage tracking
|
|
11
|
+
- Memory usage (tracemalloc)
|
|
12
|
+
- API call profiling (wall-clock time)
|
|
13
|
+
- Streaming profiling (TTFT, total time)
|
|
14
|
+
- Statistics (p50, p95, p99)
|
|
15
|
+
- cProfile integration
|
|
16
|
+
- Flamegraph generation
|
|
17
|
+
- Line-level profiling
|
|
18
|
+
- JSON/HTML export
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from praisonai.profiler import Profiler, profile, profile_imports
|
|
22
|
+
|
|
23
|
+
# Profile a function
|
|
24
|
+
@profile
|
|
25
|
+
def my_function():
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
# Profile a block
|
|
29
|
+
with Profiler.block("my_operation"):
|
|
30
|
+
do_something()
|
|
31
|
+
|
|
32
|
+
# Profile API calls
|
|
33
|
+
with Profiler.api_call("https://api.example.com") as call:
|
|
34
|
+
response = requests.get(...)
|
|
35
|
+
|
|
36
|
+
# Profile streaming
|
|
37
|
+
with Profiler.streaming("chat") as tracker:
|
|
38
|
+
tracker.first_token()
|
|
39
|
+
for chunk in stream:
|
|
40
|
+
tracker.chunk()
|
|
41
|
+
|
|
42
|
+
# Profile imports
|
|
43
|
+
with profile_imports():
|
|
44
|
+
import heavy_module
|
|
45
|
+
|
|
46
|
+
# Get report with statistics
|
|
47
|
+
Profiler.report()
|
|
48
|
+
stats = Profiler.get_statistics()
|
|
49
|
+
|
|
50
|
+
# Export
|
|
51
|
+
Profiler.export_json()
|
|
52
|
+
Profiler.export_html()
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import time
|
|
56
|
+
import functools
|
|
57
|
+
import threading
|
|
58
|
+
import sys
|
|
59
|
+
import os
|
|
60
|
+
import json
|
|
61
|
+
import tracemalloc
|
|
62
|
+
import cProfile
|
|
63
|
+
import pstats
|
|
64
|
+
import io
|
|
65
|
+
import statistics
|
|
66
|
+
from dataclasses import dataclass, field, asdict
|
|
67
|
+
from typing import Dict, List, Optional, Callable, Any
|
|
68
|
+
from contextlib import contextmanager, asynccontextmanager
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# Data Classes
|
|
73
|
+
# ============================================================================
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class TimingRecord:
|
|
77
|
+
"""Record of a single timing measurement."""
|
|
78
|
+
name: str
|
|
79
|
+
duration_ms: float
|
|
80
|
+
category: str = "function"
|
|
81
|
+
file: str = ""
|
|
82
|
+
line: int = 0
|
|
83
|
+
timestamp: float = field(default_factory=time.time)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class APICallRecord:
|
|
88
|
+
"""Record of an API/HTTP call."""
|
|
89
|
+
endpoint: str
|
|
90
|
+
method: str
|
|
91
|
+
duration_ms: float
|
|
92
|
+
status_code: int = 0
|
|
93
|
+
request_size: int = 0
|
|
94
|
+
response_size: int = 0
|
|
95
|
+
timestamp: float = field(default_factory=time.time)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class StreamingRecord:
|
|
100
|
+
"""Record of streaming operation (LLM responses)."""
|
|
101
|
+
name: str
|
|
102
|
+
ttft_ms: float # Time to first token
|
|
103
|
+
total_ms: float
|
|
104
|
+
chunk_count: int = 0
|
|
105
|
+
total_tokens: int = 0
|
|
106
|
+
timestamp: float = field(default_factory=time.time)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class MemoryRecord:
|
|
111
|
+
"""Record of memory usage."""
|
|
112
|
+
name: str
|
|
113
|
+
current_kb: float
|
|
114
|
+
peak_kb: float
|
|
115
|
+
timestamp: float = field(default_factory=time.time)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class ImportRecord:
|
|
120
|
+
"""Record of a module import."""
|
|
121
|
+
module: str
|
|
122
|
+
duration_ms: float
|
|
123
|
+
parent: str = ""
|
|
124
|
+
timestamp: float = field(default_factory=time.time)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class FlowRecord:
|
|
129
|
+
"""Record of execution flow."""
|
|
130
|
+
step: int
|
|
131
|
+
name: str
|
|
132
|
+
file: str
|
|
133
|
+
line: int
|
|
134
|
+
duration_ms: float
|
|
135
|
+
timestamp: float = field(default_factory=time.time)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ============================================================================
|
|
139
|
+
# Streaming Tracker
|
|
140
|
+
# ============================================================================
|
|
141
|
+
|
|
142
|
+
class StreamingTracker:
|
|
143
|
+
"""
|
|
144
|
+
Track streaming operations (LLM responses).
|
|
145
|
+
|
|
146
|
+
Usage:
|
|
147
|
+
tracker = StreamingTracker("chat")
|
|
148
|
+
tracker.start()
|
|
149
|
+
tracker.first_token() # Mark TTFT
|
|
150
|
+
for chunk in stream:
|
|
151
|
+
tracker.chunk()
|
|
152
|
+
tracker.end(total_tokens=100)
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, name: str):
|
|
156
|
+
self.name = name
|
|
157
|
+
self._start_time: Optional[float] = None
|
|
158
|
+
self._first_token_time: Optional[float] = None
|
|
159
|
+
self._end_time: Optional[float] = None
|
|
160
|
+
self._chunk_count: int = 0
|
|
161
|
+
self._total_tokens: int = 0
|
|
162
|
+
|
|
163
|
+
def start(self) -> None:
|
|
164
|
+
"""Start tracking."""
|
|
165
|
+
self._start_time = time.perf_counter()
|
|
166
|
+
|
|
167
|
+
def first_token(self) -> None:
|
|
168
|
+
"""Mark time to first token."""
|
|
169
|
+
if self._first_token_time is None:
|
|
170
|
+
self._first_token_time = time.perf_counter()
|
|
171
|
+
|
|
172
|
+
def chunk(self) -> None:
|
|
173
|
+
"""Record a chunk received."""
|
|
174
|
+
self._chunk_count += 1
|
|
175
|
+
|
|
176
|
+
def end(self, total_tokens: int = 0) -> None:
|
|
177
|
+
"""End tracking and record to Profiler."""
|
|
178
|
+
self._end_time = time.perf_counter()
|
|
179
|
+
self._total_tokens = total_tokens
|
|
180
|
+
|
|
181
|
+
if self._start_time is not None:
|
|
182
|
+
ttft_ms = 0.0
|
|
183
|
+
if self._first_token_time is not None:
|
|
184
|
+
ttft_ms = (self._first_token_time - self._start_time) * 1000
|
|
185
|
+
|
|
186
|
+
total_ms = (self._end_time - self._start_time) * 1000
|
|
187
|
+
|
|
188
|
+
Profiler.record_streaming(
|
|
189
|
+
name=self.name,
|
|
190
|
+
ttft_ms=ttft_ms,
|
|
191
|
+
total_ms=total_ms,
|
|
192
|
+
chunk_count=self._chunk_count,
|
|
193
|
+
total_tokens=self._total_tokens
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def ttft_ms(self) -> float:
|
|
198
|
+
"""Get time to first token in ms."""
|
|
199
|
+
if self._start_time and self._first_token_time:
|
|
200
|
+
return (self._first_token_time - self._start_time) * 1000
|
|
201
|
+
return 0.0
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def elapsed_ms(self) -> float:
|
|
205
|
+
"""Get elapsed time in ms."""
|
|
206
|
+
if self._start_time:
|
|
207
|
+
end = self._end_time or time.perf_counter()
|
|
208
|
+
return (end - self._start_time) * 1000
|
|
209
|
+
return 0.0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ============================================================================
|
|
213
|
+
# Profiler Class
|
|
214
|
+
# ============================================================================
|
|
215
|
+
|
|
216
|
+
class Profiler:
|
|
217
|
+
"""
|
|
218
|
+
Centralized profiler for performance monitoring.
|
|
219
|
+
|
|
220
|
+
Thread-safe singleton pattern for global access.
|
|
221
|
+
|
|
222
|
+
Features:
|
|
223
|
+
- Function/block timing
|
|
224
|
+
- API call profiling (wall-clock)
|
|
225
|
+
- Streaming profiling (TTFT)
|
|
226
|
+
- Memory profiling
|
|
227
|
+
- Import timing
|
|
228
|
+
- Statistics (p50, p95, p99)
|
|
229
|
+
- cProfile integration
|
|
230
|
+
- Export (JSON, HTML)
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
_instance: Optional['Profiler'] = None
|
|
234
|
+
_lock = threading.Lock()
|
|
235
|
+
|
|
236
|
+
# Class-level storage
|
|
237
|
+
_timings: List[TimingRecord] = []
|
|
238
|
+
_imports: List[ImportRecord] = []
|
|
239
|
+
_flow: List[FlowRecord] = []
|
|
240
|
+
_api_calls: List[APICallRecord] = []
|
|
241
|
+
_streaming: List[StreamingRecord] = []
|
|
242
|
+
_memory: List[MemoryRecord] = []
|
|
243
|
+
_enabled: bool = False
|
|
244
|
+
_flow_step: int = 0
|
|
245
|
+
_files_accessed: Dict[str, int] = {}
|
|
246
|
+
_line_profile_data: Dict[str, Any] = {}
|
|
247
|
+
_cprofile_stats: List[Dict[str, Any]] = []
|
|
248
|
+
|
|
249
|
+
def __new__(cls):
|
|
250
|
+
if cls._instance is None:
|
|
251
|
+
with cls._lock:
|
|
252
|
+
if cls._instance is None:
|
|
253
|
+
cls._instance = super().__new__(cls)
|
|
254
|
+
return cls._instance
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def enable(cls) -> None:
|
|
258
|
+
"""Enable profiling."""
|
|
259
|
+
cls._enabled = True
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def disable(cls) -> None:
|
|
263
|
+
"""Disable profiling."""
|
|
264
|
+
cls._enabled = False
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def is_enabled(cls) -> bool:
|
|
268
|
+
"""Check if profiling is enabled."""
|
|
269
|
+
return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def clear(cls) -> None:
|
|
273
|
+
"""Clear all profiling data."""
|
|
274
|
+
with cls._lock:
|
|
275
|
+
cls._timings.clear()
|
|
276
|
+
cls._imports.clear()
|
|
277
|
+
cls._flow.clear()
|
|
278
|
+
cls._api_calls.clear()
|
|
279
|
+
cls._streaming.clear()
|
|
280
|
+
cls._memory.clear()
|
|
281
|
+
cls._flow_step = 0
|
|
282
|
+
cls._files_accessed.clear()
|
|
283
|
+
cls._line_profile_data.clear()
|
|
284
|
+
cls._cprofile_stats.clear()
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def record_timing(cls, name: str, duration_ms: float, category: str = "function",
|
|
288
|
+
file: str = "", line: int = 0) -> None:
|
|
289
|
+
"""Record a timing measurement."""
|
|
290
|
+
if not cls.is_enabled():
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
with cls._lock:
|
|
294
|
+
cls._timings.append(TimingRecord(
|
|
295
|
+
name=name,
|
|
296
|
+
duration_ms=duration_ms,
|
|
297
|
+
category=category,
|
|
298
|
+
file=file,
|
|
299
|
+
line=line
|
|
300
|
+
))
|
|
301
|
+
|
|
302
|
+
# Track file access
|
|
303
|
+
if file:
|
|
304
|
+
cls._files_accessed[file] = cls._files_accessed.get(file, 0) + 1
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def record_import(cls, module: str, duration_ms: float, parent: str = "") -> None:
|
|
308
|
+
"""Record an import timing."""
|
|
309
|
+
if not cls.is_enabled():
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
with cls._lock:
|
|
313
|
+
cls._imports.append(ImportRecord(
|
|
314
|
+
module=module,
|
|
315
|
+
duration_ms=duration_ms,
|
|
316
|
+
parent=parent
|
|
317
|
+
))
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def record_flow(cls, name: str, duration_ms: float, file: str = "", line: int = 0) -> None:
|
|
321
|
+
"""Record a flow step."""
|
|
322
|
+
if not cls.is_enabled():
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
with cls._lock:
|
|
326
|
+
cls._flow_step += 1
|
|
327
|
+
cls._flow.append(FlowRecord(
|
|
328
|
+
step=cls._flow_step,
|
|
329
|
+
name=name,
|
|
330
|
+
file=file,
|
|
331
|
+
line=line,
|
|
332
|
+
duration_ms=duration_ms
|
|
333
|
+
))
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
@contextmanager
|
|
337
|
+
def block(cls, name: str, category: str = "block"):
|
|
338
|
+
"""Context manager for profiling a block of code."""
|
|
339
|
+
start = time.time()
|
|
340
|
+
frame = sys._getframe(2) if hasattr(sys, '_getframe') else None
|
|
341
|
+
file = frame.f_code.co_filename if frame else ""
|
|
342
|
+
line = frame.f_lineno if frame else 0
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
yield
|
|
346
|
+
finally:
|
|
347
|
+
duration_ms = (time.time() - start) * 1000
|
|
348
|
+
cls.record_timing(name, duration_ms, category, file, line)
|
|
349
|
+
cls.record_flow(name, duration_ms, file, line)
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
|
|
353
|
+
"""Get timing records, optionally filtered by category."""
|
|
354
|
+
with cls._lock:
|
|
355
|
+
if category:
|
|
356
|
+
return [t for t in cls._timings if t.category == category]
|
|
357
|
+
return cls._timings.copy()
|
|
358
|
+
|
|
359
|
+
@classmethod
|
|
360
|
+
def get_imports(cls, min_duration_ms: float = 0) -> List[ImportRecord]:
|
|
361
|
+
"""Get import records, optionally filtered by minimum duration."""
|
|
362
|
+
with cls._lock:
|
|
363
|
+
if min_duration_ms > 0:
|
|
364
|
+
return [i for i in cls._imports if i.duration_ms >= min_duration_ms]
|
|
365
|
+
return cls._imports.copy()
|
|
366
|
+
|
|
367
|
+
@classmethod
|
|
368
|
+
def get_flow(cls) -> List[FlowRecord]:
|
|
369
|
+
"""Get flow records."""
|
|
370
|
+
with cls._lock:
|
|
371
|
+
return cls._flow.copy()
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def get_files_accessed(cls) -> Dict[str, int]:
|
|
375
|
+
"""Get files accessed with counts."""
|
|
376
|
+
with cls._lock:
|
|
377
|
+
return cls._files_accessed.copy()
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def get_summary(cls) -> Dict[str, Any]:
|
|
381
|
+
"""Get profiling summary."""
|
|
382
|
+
with cls._lock:
|
|
383
|
+
total_time = sum(t.duration_ms for t in cls._timings)
|
|
384
|
+
import_time = sum(i.duration_ms for i in cls._imports)
|
|
385
|
+
|
|
386
|
+
# Group by category
|
|
387
|
+
by_category: Dict[str, float] = {}
|
|
388
|
+
for t in cls._timings:
|
|
389
|
+
by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
|
|
390
|
+
|
|
391
|
+
# Top slowest
|
|
392
|
+
slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:10]
|
|
393
|
+
slowest_imports = sorted(cls._imports, key=lambda x: x.duration_ms, reverse=True)[:10]
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
'total_time_ms': total_time,
|
|
397
|
+
'import_time_ms': import_time,
|
|
398
|
+
'timing_count': len(cls._timings),
|
|
399
|
+
'import_count': len(cls._imports),
|
|
400
|
+
'flow_steps': len(cls._flow),
|
|
401
|
+
'files_accessed': len(cls._files_accessed),
|
|
402
|
+
'by_category': by_category,
|
|
403
|
+
'slowest_operations': [(s.name, s.duration_ms) for s in slowest],
|
|
404
|
+
'slowest_imports': [(s.module, s.duration_ms) for s in slowest_imports],
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def report(cls, output: str = "console") -> str:
|
|
409
|
+
"""Generate and output profiling report."""
|
|
410
|
+
summary = cls.get_summary()
|
|
411
|
+
|
|
412
|
+
lines = [
|
|
413
|
+
"=" * 60,
|
|
414
|
+
"PraisonAI Profiling Report",
|
|
415
|
+
"=" * 60,
|
|
416
|
+
"",
|
|
417
|
+
f"Total Time: {summary['total_time_ms']:.2f}ms",
|
|
418
|
+
f"Import Time: {summary['import_time_ms']:.2f}ms",
|
|
419
|
+
f"Timing Records: {summary['timing_count']}",
|
|
420
|
+
f"Import Records: {summary['import_count']}",
|
|
421
|
+
f"Flow Steps: {summary['flow_steps']}",
|
|
422
|
+
f"Files Accessed: {summary['files_accessed']}",
|
|
423
|
+
"",
|
|
424
|
+
"By Category:",
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
for cat, time_ms in summary['by_category'].items():
|
|
428
|
+
lines.append(f" {cat}: {time_ms:.2f}ms")
|
|
429
|
+
|
|
430
|
+
lines.extend([
|
|
431
|
+
"",
|
|
432
|
+
"Slowest Operations:",
|
|
433
|
+
])
|
|
434
|
+
for name, time_ms in summary['slowest_operations']:
|
|
435
|
+
lines.append(f" {name}: {time_ms:.2f}ms")
|
|
436
|
+
|
|
437
|
+
lines.extend([
|
|
438
|
+
"",
|
|
439
|
+
"Slowest Imports:",
|
|
440
|
+
])
|
|
441
|
+
for module, time_ms in summary['slowest_imports']:
|
|
442
|
+
lines.append(f" {module}: {time_ms:.2f}ms")
|
|
443
|
+
|
|
444
|
+
lines.append("=" * 60)
|
|
445
|
+
|
|
446
|
+
report_text = "\n".join(lines)
|
|
447
|
+
|
|
448
|
+
if output == "console":
|
|
449
|
+
print(report_text)
|
|
450
|
+
|
|
451
|
+
return report_text
|
|
452
|
+
|
|
453
|
+
# ========================================================================
|
|
454
|
+
# API Call Profiling
|
|
455
|
+
# ========================================================================
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def record_api_call(cls, endpoint: str, method: str, duration_ms: float,
|
|
459
|
+
status_code: int = 0, request_size: int = 0,
|
|
460
|
+
response_size: int = 0) -> None:
|
|
461
|
+
"""Record an API/HTTP call timing."""
|
|
462
|
+
if not cls.is_enabled():
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
with cls._lock:
|
|
466
|
+
cls._api_calls.append(APICallRecord(
|
|
467
|
+
endpoint=endpoint,
|
|
468
|
+
method=method,
|
|
469
|
+
duration_ms=duration_ms,
|
|
470
|
+
status_code=status_code,
|
|
471
|
+
request_size=request_size,
|
|
472
|
+
response_size=response_size
|
|
473
|
+
))
|
|
474
|
+
|
|
475
|
+
@classmethod
|
|
476
|
+
def get_api_calls(cls) -> List[APICallRecord]:
|
|
477
|
+
"""Get API call records."""
|
|
478
|
+
with cls._lock:
|
|
479
|
+
return cls._api_calls.copy()
|
|
480
|
+
|
|
481
|
+
@classmethod
|
|
482
|
+
@contextmanager
|
|
483
|
+
def api_call(cls, endpoint: str, method: str = "GET"):
|
|
484
|
+
"""Context manager for profiling API calls."""
|
|
485
|
+
start = time.perf_counter()
|
|
486
|
+
call_info = {'status_code': 0, 'request_size': 0, 'response_size': 0}
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
yield call_info
|
|
490
|
+
finally:
|
|
491
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
492
|
+
cls.record_api_call(
|
|
493
|
+
endpoint=endpoint,
|
|
494
|
+
method=method,
|
|
495
|
+
duration_ms=duration_ms,
|
|
496
|
+
status_code=call_info.get('status_code', 0),
|
|
497
|
+
request_size=call_info.get('request_size', 0),
|
|
498
|
+
response_size=call_info.get('response_size', 0)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# ========================================================================
|
|
502
|
+
# Streaming Profiling
|
|
503
|
+
# ========================================================================
|
|
504
|
+
|
|
505
|
+
@classmethod
|
|
506
|
+
def record_streaming(cls, name: str, ttft_ms: float, total_ms: float,
|
|
507
|
+
chunk_count: int = 0, total_tokens: int = 0) -> None:
|
|
508
|
+
"""Record streaming metrics."""
|
|
509
|
+
if not cls.is_enabled():
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
with cls._lock:
|
|
513
|
+
cls._streaming.append(StreamingRecord(
|
|
514
|
+
name=name,
|
|
515
|
+
ttft_ms=ttft_ms,
|
|
516
|
+
total_ms=total_ms,
|
|
517
|
+
chunk_count=chunk_count,
|
|
518
|
+
total_tokens=total_tokens
|
|
519
|
+
))
|
|
520
|
+
|
|
521
|
+
@classmethod
|
|
522
|
+
def get_streaming_records(cls) -> List[StreamingRecord]:
|
|
523
|
+
"""Get streaming records."""
|
|
524
|
+
with cls._lock:
|
|
525
|
+
return cls._streaming.copy()
|
|
526
|
+
|
|
527
|
+
@classmethod
|
|
528
|
+
@contextmanager
|
|
529
|
+
def streaming(cls, name: str):
|
|
530
|
+
"""Context manager for profiling streaming operations."""
|
|
531
|
+
tracker = StreamingTracker(name)
|
|
532
|
+
tracker.start()
|
|
533
|
+
try:
|
|
534
|
+
yield tracker
|
|
535
|
+
finally:
|
|
536
|
+
tracker.end()
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
@asynccontextmanager
|
|
540
|
+
async def streaming_async(cls, name: str):
|
|
541
|
+
"""Async context manager for profiling streaming operations."""
|
|
542
|
+
tracker = StreamingTracker(name)
|
|
543
|
+
tracker.start()
|
|
544
|
+
try:
|
|
545
|
+
yield tracker
|
|
546
|
+
finally:
|
|
547
|
+
tracker.end()
|
|
548
|
+
|
|
549
|
+
# ========================================================================
|
|
550
|
+
# Memory Profiling
|
|
551
|
+
# ========================================================================
|
|
552
|
+
|
|
553
|
+
@classmethod
|
|
554
|
+
def record_memory(cls, name: str, current_kb: float, peak_kb: float) -> None:
|
|
555
|
+
"""Record memory usage."""
|
|
556
|
+
if not cls.is_enabled():
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
with cls._lock:
|
|
560
|
+
cls._memory.append(MemoryRecord(
|
|
561
|
+
name=name,
|
|
562
|
+
current_kb=current_kb,
|
|
563
|
+
peak_kb=peak_kb
|
|
564
|
+
))
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def get_memory_records(cls) -> List[MemoryRecord]:
|
|
568
|
+
"""Get memory records."""
|
|
569
|
+
with cls._lock:
|
|
570
|
+
return cls._memory.copy()
|
|
571
|
+
|
|
572
|
+
@classmethod
|
|
573
|
+
@contextmanager
|
|
574
|
+
def memory(cls, name: str):
|
|
575
|
+
"""Context manager for profiling memory usage."""
|
|
576
|
+
tracemalloc.start()
|
|
577
|
+
try:
|
|
578
|
+
yield
|
|
579
|
+
finally:
|
|
580
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
581
|
+
tracemalloc.stop()
|
|
582
|
+
cls.record_memory(name, current / 1024, peak / 1024)
|
|
583
|
+
|
|
584
|
+
@classmethod
|
|
585
|
+
def memory_snapshot(cls) -> Dict[str, float]:
|
|
586
|
+
"""Take a memory snapshot."""
|
|
587
|
+
tracemalloc.start()
|
|
588
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
589
|
+
tracemalloc.stop()
|
|
590
|
+
return {
|
|
591
|
+
'current_kb': current / 1024,
|
|
592
|
+
'peak_kb': peak / 1024
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# ========================================================================
|
|
596
|
+
# Statistics
|
|
597
|
+
# ========================================================================
|
|
598
|
+
|
|
599
|
+
@classmethod
|
|
600
|
+
def get_statistics(cls, category: Optional[str] = None) -> Dict[str, float]:
|
|
601
|
+
"""
|
|
602
|
+
Get statistical analysis of timing data.
|
|
603
|
+
|
|
604
|
+
Returns p50, p95, p99, mean, std_dev, min, max.
|
|
605
|
+
"""
|
|
606
|
+
with cls._lock:
|
|
607
|
+
if category:
|
|
608
|
+
durations = [t.duration_ms for t in cls._timings if t.category == category]
|
|
609
|
+
else:
|
|
610
|
+
durations = [t.duration_ms for t in cls._timings]
|
|
611
|
+
|
|
612
|
+
if not durations:
|
|
613
|
+
return {
|
|
614
|
+
'p50': 0.0, 'p95': 0.0, 'p99': 0.0,
|
|
615
|
+
'mean': 0.0, 'std_dev': 0.0, 'min': 0.0, 'max': 0.0,
|
|
616
|
+
'count': 0
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
sorted_durations = sorted(durations)
|
|
620
|
+
n = len(sorted_durations)
|
|
621
|
+
|
|
622
|
+
def percentile(p: float) -> float:
|
|
623
|
+
idx = int(n * p / 100)
|
|
624
|
+
return sorted_durations[min(idx, n - 1)]
|
|
625
|
+
|
|
626
|
+
mean = statistics.mean(durations)
|
|
627
|
+
std_dev = statistics.stdev(durations) if n > 1 else 0.0
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
'p50': percentile(50),
|
|
631
|
+
'p95': percentile(95),
|
|
632
|
+
'p99': percentile(99),
|
|
633
|
+
'mean': mean,
|
|
634
|
+
'std_dev': std_dev,
|
|
635
|
+
'min': min(durations),
|
|
636
|
+
'max': max(durations),
|
|
637
|
+
'count': n
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# ========================================================================
|
|
641
|
+
# cProfile Integration
|
|
642
|
+
# ========================================================================
|
|
643
|
+
|
|
644
|
+
@classmethod
|
|
645
|
+
@contextmanager
|
|
646
|
+
def cprofile(cls, name: str):
|
|
647
|
+
"""Context manager for cProfile profiling."""
|
|
648
|
+
pr = cProfile.Profile()
|
|
649
|
+
pr.enable()
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
yield pr
|
|
653
|
+
finally:
|
|
654
|
+
pr.disable()
|
|
655
|
+
|
|
656
|
+
# Store stats
|
|
657
|
+
s = io.StringIO()
|
|
658
|
+
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
|
|
659
|
+
ps.print_stats(30)
|
|
660
|
+
|
|
661
|
+
with cls._lock:
|
|
662
|
+
cls._cprofile_stats.append({
|
|
663
|
+
'name': name,
|
|
664
|
+
'stats': s.getvalue(),
|
|
665
|
+
'total_calls': ps.total_calls if hasattr(ps, 'total_calls') else 0,
|
|
666
|
+
'timestamp': time.time()
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
@classmethod
|
|
670
|
+
def get_cprofile_stats(cls) -> List[Dict[str, Any]]:
|
|
671
|
+
"""Get cProfile statistics."""
|
|
672
|
+
with cls._lock:
|
|
673
|
+
return cls._cprofile_stats.copy()
|
|
674
|
+
|
|
675
|
+
# ========================================================================
|
|
676
|
+
# Line-Level Profiling
|
|
677
|
+
# ========================================================================
|
|
678
|
+
|
|
679
|
+
@classmethod
|
|
680
|
+
def get_line_profile_data(cls) -> Dict[str, Any]:
|
|
681
|
+
"""Get line-level profiling data."""
|
|
682
|
+
with cls._lock:
|
|
683
|
+
return cls._line_profile_data.copy()
|
|
684
|
+
|
|
685
|
+
@classmethod
|
|
686
|
+
def set_line_profile_data(cls, func_name: str, data: Any) -> None:
|
|
687
|
+
"""Store line-level profiling data."""
|
|
688
|
+
if not cls.is_enabled():
|
|
689
|
+
return
|
|
690
|
+
with cls._lock:
|
|
691
|
+
cls._line_profile_data[func_name] = data
|
|
692
|
+
|
|
693
|
+
# ========================================================================
|
|
694
|
+
# Flamegraph
|
|
695
|
+
# ========================================================================
|
|
696
|
+
|
|
697
|
+
@classmethod
|
|
698
|
+
def get_flamegraph_data(cls) -> List[Dict[str, Any]]:
|
|
699
|
+
"""
|
|
700
|
+
Generate flamegraph-compatible data from flow records.
|
|
701
|
+
|
|
702
|
+
Returns list of {name, value, children} for flamegraph visualization.
|
|
703
|
+
"""
|
|
704
|
+
with cls._lock:
|
|
705
|
+
# Convert flow records to flamegraph format
|
|
706
|
+
data = []
|
|
707
|
+
for record in cls._flow:
|
|
708
|
+
data.append({
|
|
709
|
+
'name': record.name,
|
|
710
|
+
'value': record.duration_ms,
|
|
711
|
+
'file': record.file,
|
|
712
|
+
'line': record.line
|
|
713
|
+
})
|
|
714
|
+
return data
|
|
715
|
+
|
|
716
|
+
@classmethod
|
|
717
|
+
def export_flamegraph(cls, filepath: str) -> None:
|
|
718
|
+
"""
|
|
719
|
+
Export flamegraph to SVG file.
|
|
720
|
+
|
|
721
|
+
Note: Requires flamegraph data. For full flamegraph support,
|
|
722
|
+
use py-spy: py-spy record -o profile.svg -- python script.py
|
|
723
|
+
"""
|
|
724
|
+
data = cls.get_flamegraph_data()
|
|
725
|
+
|
|
726
|
+
# Generate simple SVG flamegraph
|
|
727
|
+
svg_content = cls._generate_simple_flamegraph_svg(data)
|
|
728
|
+
|
|
729
|
+
with open(filepath, 'w') as f:
|
|
730
|
+
f.write(svg_content)
|
|
731
|
+
|
|
732
|
+
@classmethod
|
|
733
|
+
def _generate_simple_flamegraph_svg(cls, data: List[Dict[str, Any]]) -> str:
|
|
734
|
+
"""Generate a simple SVG flamegraph."""
|
|
735
|
+
if not data:
|
|
736
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="100"><text x="10" y="50">No profiling data</text></svg>'
|
|
737
|
+
|
|
738
|
+
total_time = sum(d['value'] for d in data)
|
|
739
|
+
if total_time == 0:
|
|
740
|
+
total_time = 1
|
|
741
|
+
|
|
742
|
+
width = 800
|
|
743
|
+
height = max(100, len(data) * 25 + 50)
|
|
744
|
+
|
|
745
|
+
svg_parts = [
|
|
746
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">',
|
|
747
|
+
'<style>rect:hover { opacity: 0.8; } text { font-family: monospace; font-size: 12px; }</style>',
|
|
748
|
+
f'<text x="10" y="20">PraisonAI Profiling Flamegraph (Total: {total_time:.2f}ms)</text>'
|
|
749
|
+
]
|
|
750
|
+
|
|
751
|
+
y = 40
|
|
752
|
+
for item in sorted(data, key=lambda x: x['value'], reverse=True)[:20]:
|
|
753
|
+
bar_width = max(10, (item['value'] / total_time) * (width - 20))
|
|
754
|
+
color = f'hsl({int(item["value"] / total_time * 120)}, 70%, 50%)'
|
|
755
|
+
|
|
756
|
+
svg_parts.append(
|
|
757
|
+
f'<rect x="10" y="{y}" width="{bar_width}" height="20" fill="{color}" />'
|
|
758
|
+
)
|
|
759
|
+
svg_parts.append(
|
|
760
|
+
f'<text x="15" y="{y + 15}">{item["name"]}: {item["value"]:.2f}ms</text>'
|
|
761
|
+
)
|
|
762
|
+
y += 25
|
|
763
|
+
|
|
764
|
+
svg_parts.append('</svg>')
|
|
765
|
+
return '\n'.join(svg_parts)
|
|
766
|
+
|
|
767
|
+
# ========================================================================
|
|
768
|
+
# Export Functions
|
|
769
|
+
# ========================================================================
|
|
770
|
+
|
|
771
|
+
@classmethod
|
|
772
|
+
def export_json(cls) -> str:
|
|
773
|
+
"""Export profiling data as JSON."""
|
|
774
|
+
# Get summary and stats first (they acquire their own locks)
|
|
775
|
+
summary = cls.get_summary()
|
|
776
|
+
stats = cls.get_statistics()
|
|
777
|
+
|
|
778
|
+
with cls._lock:
|
|
779
|
+
data = {
|
|
780
|
+
'summary': summary,
|
|
781
|
+
'statistics': stats,
|
|
782
|
+
'timings': [asdict(t) for t in cls._timings],
|
|
783
|
+
'api_calls': [asdict(a) for a in cls._api_calls],
|
|
784
|
+
'streaming': [asdict(s) for s in cls._streaming],
|
|
785
|
+
'memory': [asdict(m) for m in cls._memory],
|
|
786
|
+
'imports': [asdict(i) for i in cls._imports],
|
|
787
|
+
'flow': [asdict(f) for f in cls._flow]
|
|
788
|
+
}
|
|
789
|
+
return json.dumps(data, indent=2, default=str)
|
|
790
|
+
|
|
791
|
+
@classmethod
|
|
792
|
+
def export_html(cls) -> str:
|
|
793
|
+
"""Export profiling data as HTML report."""
|
|
794
|
+
summary = cls.get_summary()
|
|
795
|
+
stats = cls.get_statistics()
|
|
796
|
+
|
|
797
|
+
html = f'''<!DOCTYPE html>
|
|
798
|
+
<html>
|
|
799
|
+
<head>
|
|
800
|
+
<title>PraisonAI Profiling Report</title>
|
|
801
|
+
<style>
|
|
802
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; }}
|
|
803
|
+
h1 {{ color: #333; }}
|
|
804
|
+
h2 {{ color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px; }}
|
|
805
|
+
table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
|
|
806
|
+
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
|
|
807
|
+
th {{ background-color: #f5f5f5; }}
|
|
808
|
+
.metric {{ display: inline-block; margin: 10px; padding: 15px; background: #f9f9f9; border-radius: 5px; }}
|
|
809
|
+
.metric-value {{ font-size: 24px; font-weight: bold; color: #0066cc; }}
|
|
810
|
+
.metric-label {{ font-size: 12px; color: #666; }}
|
|
811
|
+
</style>
|
|
812
|
+
</head>
|
|
813
|
+
<body>
|
|
814
|
+
<h1>PraisonAI Profiling Report</h1>
|
|
815
|
+
|
|
816
|
+
<h2>Summary</h2>
|
|
817
|
+
<div class="metric">
|
|
818
|
+
<div class="metric-value">{summary['total_time_ms']:.2f}ms</div>
|
|
819
|
+
<div class="metric-label">Total Time</div>
|
|
820
|
+
</div>
|
|
821
|
+
<div class="metric">
|
|
822
|
+
<div class="metric-value">{summary['timing_count']}</div>
|
|
823
|
+
<div class="metric-label">Operations</div>
|
|
824
|
+
</div>
|
|
825
|
+
<div class="metric">
|
|
826
|
+
<div class="metric-value">{len(cls._api_calls)}</div>
|
|
827
|
+
<div class="metric-label">API Calls</div>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="metric">
|
|
830
|
+
<div class="metric-value">{len(cls._streaming)}</div>
|
|
831
|
+
<div class="metric-label">Streams</div>
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
<h2>Statistics</h2>
|
|
835
|
+
<table>
|
|
836
|
+
<tr><th>Metric</th><th>Value</th></tr>
|
|
837
|
+
<tr><td>P50 (Median)</td><td>{stats['p50']:.2f}ms</td></tr>
|
|
838
|
+
<tr><td>P95</td><td>{stats['p95']:.2f}ms</td></tr>
|
|
839
|
+
<tr><td>P99</td><td>{stats['p99']:.2f}ms</td></tr>
|
|
840
|
+
<tr><td>Mean</td><td>{stats['mean']:.2f}ms</td></tr>
|
|
841
|
+
<tr><td>Std Dev</td><td>{stats['std_dev']:.2f}ms</td></tr>
|
|
842
|
+
<tr><td>Min</td><td>{stats['min']:.2f}ms</td></tr>
|
|
843
|
+
<tr><td>Max</td><td>{stats['max']:.2f}ms</td></tr>
|
|
844
|
+
</table>
|
|
845
|
+
|
|
846
|
+
<h2>Slowest Operations</h2>
|
|
847
|
+
<table>
|
|
848
|
+
<tr><th>Operation</th><th>Duration (ms)</th></tr>
|
|
849
|
+
{''.join(f"<tr><td>{name}</td><td>{dur:.2f}</td></tr>" for name, dur in summary['slowest_operations'])}
|
|
850
|
+
</table>
|
|
851
|
+
|
|
852
|
+
<h2>API Calls</h2>
|
|
853
|
+
<table>
|
|
854
|
+
<tr><th>Endpoint</th><th>Method</th><th>Duration (ms)</th><th>Status</th></tr>
|
|
855
|
+
{''.join(f"<tr><td>{a.endpoint}</td><td>{a.method}</td><td>{a.duration_ms:.2f}</td><td>{a.status_code}</td></tr>" for a in cls._api_calls[:20])}
|
|
856
|
+
</table>
|
|
857
|
+
|
|
858
|
+
<h2>Streaming</h2>
|
|
859
|
+
<table>
|
|
860
|
+
<tr><th>Name</th><th>TTFT (ms)</th><th>Total (ms)</th><th>Chunks</th><th>Tokens</th></tr>
|
|
861
|
+
{''.join(f"<tr><td>{s.name}</td><td>{s.ttft_ms:.2f}</td><td>{s.total_ms:.2f}</td><td>{s.chunk_count}</td><td>{s.total_tokens}</td></tr>" for s in cls._streaming[:20])}
|
|
862
|
+
</table>
|
|
863
|
+
</body>
|
|
864
|
+
</html>'''
|
|
865
|
+
return html
|
|
866
|
+
|
|
867
|
+
@classmethod
|
|
868
|
+
def export_to_file(cls, filepath: str, format: str = "json") -> None:
|
|
869
|
+
"""Export profiling data to file."""
|
|
870
|
+
if format == "json":
|
|
871
|
+
content = cls.export_json()
|
|
872
|
+
elif format == "html":
|
|
873
|
+
content = cls.export_html()
|
|
874
|
+
else:
|
|
875
|
+
raise ValueError(f"Unknown format: {format}")
|
|
876
|
+
|
|
877
|
+
with open(filepath, 'w') as f:
|
|
878
|
+
f.write(content)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
# ============================================================================
|
|
882
|
+
# Decorators
|
|
883
|
+
# ============================================================================
|
|
884
|
+
|
|
885
|
+
def profile(func: Optional[Callable] = None, *, category: str = "function"):
|
|
886
|
+
"""
|
|
887
|
+
Decorator to profile a function.
|
|
888
|
+
|
|
889
|
+
Usage:
|
|
890
|
+
@profile
|
|
891
|
+
def my_function():
|
|
892
|
+
pass
|
|
893
|
+
|
|
894
|
+
@profile(category="api")
|
|
895
|
+
def api_call():
|
|
896
|
+
pass
|
|
897
|
+
"""
|
|
898
|
+
def decorator(fn: Callable) -> Callable:
|
|
899
|
+
@functools.wraps(fn)
|
|
900
|
+
def wrapper(*args, **kwargs):
|
|
901
|
+
if not Profiler.is_enabled():
|
|
902
|
+
return fn(*args, **kwargs)
|
|
903
|
+
|
|
904
|
+
start = time.time()
|
|
905
|
+
try:
|
|
906
|
+
return fn(*args, **kwargs)
|
|
907
|
+
finally:
|
|
908
|
+
duration_ms = (time.time() - start) * 1000
|
|
909
|
+
file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
|
|
910
|
+
line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
|
|
911
|
+
Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
|
|
912
|
+
Profiler.record_flow(fn.__name__, duration_ms, file, line)
|
|
913
|
+
|
|
914
|
+
return wrapper
|
|
915
|
+
|
|
916
|
+
if func is not None:
|
|
917
|
+
return decorator(func)
|
|
918
|
+
return decorator
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
|
|
922
|
+
"""
|
|
923
|
+
Decorator to profile an async function.
|
|
924
|
+
"""
|
|
925
|
+
def decorator(fn: Callable) -> Callable:
|
|
926
|
+
@functools.wraps(fn)
|
|
927
|
+
async def wrapper(*args, **kwargs):
|
|
928
|
+
if not Profiler.is_enabled():
|
|
929
|
+
return await fn(*args, **kwargs)
|
|
930
|
+
|
|
931
|
+
start = time.time()
|
|
932
|
+
try:
|
|
933
|
+
return await fn(*args, **kwargs)
|
|
934
|
+
finally:
|
|
935
|
+
duration_ms = (time.time() - start) * 1000
|
|
936
|
+
file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
|
|
937
|
+
line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
|
|
938
|
+
Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
|
|
939
|
+
Profiler.record_flow(fn.__name__, duration_ms, file, line)
|
|
940
|
+
|
|
941
|
+
return wrapper
|
|
942
|
+
|
|
943
|
+
if func is not None:
|
|
944
|
+
return decorator(func)
|
|
945
|
+
return decorator
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
# ============================================================================
|
|
949
|
+
# Import Profiling
|
|
950
|
+
# ============================================================================
|
|
951
|
+
|
|
952
|
+
class ImportProfiler:
|
|
953
|
+
"""
|
|
954
|
+
Context manager to profile imports.
|
|
955
|
+
|
|
956
|
+
Usage:
|
|
957
|
+
with profile_imports() as profiler:
|
|
958
|
+
import heavy_module
|
|
959
|
+
|
|
960
|
+
print(profiler.get_imports())
|
|
961
|
+
"""
|
|
962
|
+
|
|
963
|
+
def __init__(self):
|
|
964
|
+
self._original_import = None
|
|
965
|
+
self._imports: List[ImportRecord] = []
|
|
966
|
+
|
|
967
|
+
def __enter__(self):
|
|
968
|
+
import builtins
|
|
969
|
+
self._original_import = builtins.__import__
|
|
970
|
+
|
|
971
|
+
def profiled_import(name, globals=None, locals=None, fromlist=(), level=0):
|
|
972
|
+
start = time.time()
|
|
973
|
+
try:
|
|
974
|
+
return self._original_import(name, globals, locals, fromlist, level)
|
|
975
|
+
finally:
|
|
976
|
+
duration_ms = (time.time() - start) * 1000
|
|
977
|
+
if duration_ms > 1: # Only record imports > 1ms
|
|
978
|
+
record = ImportRecord(module=name, duration_ms=duration_ms)
|
|
979
|
+
self._imports.append(record)
|
|
980
|
+
Profiler.record_import(name, duration_ms)
|
|
981
|
+
|
|
982
|
+
builtins.__import__ = profiled_import
|
|
983
|
+
return self
|
|
984
|
+
|
|
985
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
986
|
+
import builtins
|
|
987
|
+
builtins.__import__ = self._original_import
|
|
988
|
+
return False
|
|
989
|
+
|
|
990
|
+
def get_imports(self, min_duration_ms: float = 0) -> List[ImportRecord]:
|
|
991
|
+
"""Get recorded imports."""
|
|
992
|
+
if min_duration_ms > 0:
|
|
993
|
+
return [i for i in self._imports if i.duration_ms >= min_duration_ms]
|
|
994
|
+
return self._imports.copy()
|
|
995
|
+
|
|
996
|
+
def get_slowest(self, n: int = 10) -> List[ImportRecord]:
|
|
997
|
+
"""Get N slowest imports."""
|
|
998
|
+
return sorted(self._imports, key=lambda x: x.duration_ms, reverse=True)[:n]
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def profile_imports():
|
|
1002
|
+
"""Create an import profiler context manager."""
|
|
1003
|
+
return ImportProfiler()
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
# ============================================================================
|
|
1007
|
+
# API Profiling Decorators
|
|
1008
|
+
# ============================================================================
|
|
1009
|
+
|
|
1010
|
+
def profile_api(func: Optional[Callable] = None, *, endpoint: str = ""):
|
|
1011
|
+
"""
|
|
1012
|
+
Decorator to profile a function as an API call.
|
|
1013
|
+
|
|
1014
|
+
Usage:
|
|
1015
|
+
@profile_api(endpoint="openai/chat")
|
|
1016
|
+
def call_openai():
|
|
1017
|
+
pass
|
|
1018
|
+
"""
|
|
1019
|
+
def decorator(fn: Callable) -> Callable:
|
|
1020
|
+
@functools.wraps(fn)
|
|
1021
|
+
def wrapper(*args, **kwargs):
|
|
1022
|
+
if not Profiler.is_enabled():
|
|
1023
|
+
return fn(*args, **kwargs)
|
|
1024
|
+
|
|
1025
|
+
ep = endpoint or fn.__name__
|
|
1026
|
+
start = time.perf_counter()
|
|
1027
|
+
try:
|
|
1028
|
+
return fn(*args, **kwargs)
|
|
1029
|
+
finally:
|
|
1030
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
1031
|
+
Profiler.record_api_call(ep, "CALL", duration_ms)
|
|
1032
|
+
|
|
1033
|
+
return wrapper
|
|
1034
|
+
|
|
1035
|
+
if func is not None:
|
|
1036
|
+
return decorator(func)
|
|
1037
|
+
return decorator
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def profile_api_async(func: Optional[Callable] = None, *, endpoint: str = ""):
|
|
1041
|
+
"""
|
|
1042
|
+
Decorator to profile an async function as an API call.
|
|
1043
|
+
"""
|
|
1044
|
+
def decorator(fn: Callable) -> Callable:
|
|
1045
|
+
@functools.wraps(fn)
|
|
1046
|
+
async def wrapper(*args, **kwargs):
|
|
1047
|
+
if not Profiler.is_enabled():
|
|
1048
|
+
return await fn(*args, **kwargs)
|
|
1049
|
+
|
|
1050
|
+
ep = endpoint or fn.__name__
|
|
1051
|
+
start = time.perf_counter()
|
|
1052
|
+
try:
|
|
1053
|
+
return await fn(*args, **kwargs)
|
|
1054
|
+
finally:
|
|
1055
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
1056
|
+
Profiler.record_api_call(ep, "CALL", duration_ms)
|
|
1057
|
+
|
|
1058
|
+
return wrapper
|
|
1059
|
+
|
|
1060
|
+
if func is not None:
|
|
1061
|
+
return decorator(func)
|
|
1062
|
+
return decorator
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
# ============================================================================
|
|
1066
|
+
# cProfile Decorator
|
|
1067
|
+
# ============================================================================
|
|
1068
|
+
|
|
1069
|
+
def profile_detailed(func: Optional[Callable] = None):
|
|
1070
|
+
"""
|
|
1071
|
+
Decorator for detailed cProfile profiling.
|
|
1072
|
+
|
|
1073
|
+
Usage:
|
|
1074
|
+
@profile_detailed
|
|
1075
|
+
def heavy_computation():
|
|
1076
|
+
pass
|
|
1077
|
+
"""
|
|
1078
|
+
def decorator(fn: Callable) -> Callable:
|
|
1079
|
+
@functools.wraps(fn)
|
|
1080
|
+
def wrapper(*args, **kwargs):
|
|
1081
|
+
if not Profiler.is_enabled():
|
|
1082
|
+
return fn(*args, **kwargs)
|
|
1083
|
+
|
|
1084
|
+
pr = cProfile.Profile()
|
|
1085
|
+
pr.enable()
|
|
1086
|
+
try:
|
|
1087
|
+
return fn(*args, **kwargs)
|
|
1088
|
+
finally:
|
|
1089
|
+
pr.disable()
|
|
1090
|
+
s = io.StringIO()
|
|
1091
|
+
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
|
|
1092
|
+
ps.print_stats(20)
|
|
1093
|
+
Profiler._cprofile_stats.append({
|
|
1094
|
+
'name': fn.__name__,
|
|
1095
|
+
'stats': s.getvalue(),
|
|
1096
|
+
'timestamp': time.time()
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
return wrapper
|
|
1100
|
+
|
|
1101
|
+
if func is not None:
|
|
1102
|
+
return decorator(func)
|
|
1103
|
+
return decorator
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
# ============================================================================
|
|
1107
|
+
# Line-Level Profiling Decorator
|
|
1108
|
+
# ============================================================================
|
|
1109
|
+
|
|
1110
|
+
def profile_lines(func: Optional[Callable] = None):
|
|
1111
|
+
"""
|
|
1112
|
+
Decorator for line-level profiling.
|
|
1113
|
+
|
|
1114
|
+
Note: Requires line_profiler package for full functionality.
|
|
1115
|
+
Falls back to basic timing if not available.
|
|
1116
|
+
|
|
1117
|
+
Usage:
|
|
1118
|
+
@profile_lines
|
|
1119
|
+
def my_function():
|
|
1120
|
+
pass
|
|
1121
|
+
"""
|
|
1122
|
+
def decorator(fn: Callable) -> Callable:
|
|
1123
|
+
@functools.wraps(fn)
|
|
1124
|
+
def wrapper(*args, **kwargs):
|
|
1125
|
+
if not Profiler.is_enabled():
|
|
1126
|
+
return fn(*args, **kwargs)
|
|
1127
|
+
|
|
1128
|
+
# Try to use line_profiler if available
|
|
1129
|
+
try:
|
|
1130
|
+
from line_profiler import LineProfiler
|
|
1131
|
+
lp = LineProfiler()
|
|
1132
|
+
lp.add_function(fn)
|
|
1133
|
+
lp.enable()
|
|
1134
|
+
try:
|
|
1135
|
+
result = fn(*args, **kwargs)
|
|
1136
|
+
finally:
|
|
1137
|
+
lp.disable()
|
|
1138
|
+
s = io.StringIO()
|
|
1139
|
+
lp.print_stats(stream=s)
|
|
1140
|
+
Profiler.set_line_profile_data(fn.__name__, s.getvalue())
|
|
1141
|
+
return result
|
|
1142
|
+
except ImportError:
|
|
1143
|
+
# Fallback to basic timing
|
|
1144
|
+
start = time.perf_counter()
|
|
1145
|
+
try:
|
|
1146
|
+
return fn(*args, **kwargs)
|
|
1147
|
+
finally:
|
|
1148
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
1149
|
+
Profiler.set_line_profile_data(fn.__name__, {
|
|
1150
|
+
'note': 'line_profiler not installed',
|
|
1151
|
+
'total_ms': duration_ms
|
|
1152
|
+
})
|
|
1153
|
+
|
|
1154
|
+
return wrapper
|
|
1155
|
+
|
|
1156
|
+
if func is not None:
|
|
1157
|
+
return decorator(func)
|
|
1158
|
+
return decorator
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
# ============================================================================
|
|
1162
|
+
# Quick Profiling Functions
|
|
1163
|
+
# ============================================================================
|
|
1164
|
+
|
|
1165
|
+
def time_import(module_name: str) -> float:
|
|
1166
|
+
"""
|
|
1167
|
+
Time how long it takes to import a module.
|
|
1168
|
+
|
|
1169
|
+
Returns duration in milliseconds.
|
|
1170
|
+
"""
|
|
1171
|
+
start = time.time()
|
|
1172
|
+
__import__(module_name)
|
|
1173
|
+
return (time.time() - start) * 1000
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def check_module_available(module_name: str) -> bool:
|
|
1177
|
+
"""
|
|
1178
|
+
Check if a module is available without importing it.
|
|
1179
|
+
|
|
1180
|
+
Uses importlib.util.find_spec which is fast.
|
|
1181
|
+
"""
|
|
1182
|
+
import importlib.util
|
|
1183
|
+
return importlib.util.find_spec(module_name) is not None
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
# ============================================================================
|
|
1187
|
+
# Exports
|
|
1188
|
+
# ============================================================================
|
|
1189
|
+
|
|
1190
|
+
__all__ = [
|
|
1191
|
+
# Core
|
|
1192
|
+
'Profiler',
|
|
1193
|
+
'StreamingTracker',
|
|
1194
|
+
# Decorators
|
|
1195
|
+
'profile',
|
|
1196
|
+
'profile_async',
|
|
1197
|
+
'profile_api',
|
|
1198
|
+
'profile_api_async',
|
|
1199
|
+
'profile_detailed',
|
|
1200
|
+
'profile_lines',
|
|
1201
|
+
# Import profiling
|
|
1202
|
+
'profile_imports',
|
|
1203
|
+
'ImportProfiler',
|
|
1204
|
+
# Utilities
|
|
1205
|
+
'time_import',
|
|
1206
|
+
'check_module_available',
|
|
1207
|
+
# Data classes
|
|
1208
|
+
'TimingRecord',
|
|
1209
|
+
'ImportRecord',
|
|
1210
|
+
'FlowRecord',
|
|
1211
|
+
'APICallRecord',
|
|
1212
|
+
'StreamingRecord',
|
|
1213
|
+
'MemoryRecord',
|
|
1214
|
+
]
|