oscura 0.5.0__py3-none-any.whl → 0.6.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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
"""Comprehensive performance profiling for analysis workflows.
|
|
2
|
+
|
|
3
|
+
This module provides detailed profiling capabilities including CPU usage,
|
|
4
|
+
memory consumption, I/O operations, and bottleneck identification using
|
|
5
|
+
multiple profiling backends (cProfile, line_profiler, memory_profiler).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> profiler = PerformanceProfiler()
|
|
9
|
+
>>> profiler.start()
|
|
10
|
+
>>> # Run analysis code here
|
|
11
|
+
>>> result = profiler.stop()
|
|
12
|
+
>>> print(result.summary())
|
|
13
|
+
>>> result.export_json("profile.json")
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
Python Profilers: https://docs.python.org/3/library/profile.html
|
|
17
|
+
IEEE 1685-2009: IP-XACT standard for profiling
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import cProfile
|
|
23
|
+
import functools
|
|
24
|
+
import io
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import pstats
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
import tracemalloc
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from dataclasses import asdict, dataclass, field
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import TYPE_CHECKING, Any, TypeAlias
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable, Iterator
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"FunctionStats",
|
|
44
|
+
"PerformanceProfiler",
|
|
45
|
+
"ProfilingMode",
|
|
46
|
+
"ProfilingResult",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Type aliases
|
|
50
|
+
HotspotDict: TypeAlias = dict[str, Any]
|
|
51
|
+
CallGraphDict: TypeAlias = dict[str, list[str]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProfilingMode(Enum):
|
|
55
|
+
"""Profiling mode selection.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
FUNCTION: Function-level profiling (time per function, call counts)
|
|
59
|
+
LINE: Line-level profiling (time per line of code)
|
|
60
|
+
MEMORY: Memory profiling (allocation tracking, memory leaks)
|
|
61
|
+
IO: I/O profiling (disk reads/writes, network traffic)
|
|
62
|
+
FULL: All profiling modes combined
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
FUNCTION = "function"
|
|
66
|
+
LINE = "line"
|
|
67
|
+
MEMORY = "memory"
|
|
68
|
+
IO = "io"
|
|
69
|
+
FULL = "full"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class FunctionStats:
|
|
74
|
+
"""Statistics for a profiled function.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
name: Function name
|
|
78
|
+
calls: Number of times called
|
|
79
|
+
time: Total time spent in function (seconds)
|
|
80
|
+
cumulative_time: Cumulative time including sub-calls (seconds)
|
|
81
|
+
memory: Peak memory usage (bytes)
|
|
82
|
+
per_call_time: Average time per call (seconds)
|
|
83
|
+
per_call_memory: Average memory per call (bytes)
|
|
84
|
+
filename: Source file containing function
|
|
85
|
+
lineno: Line number where function is defined
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
calls: int
|
|
90
|
+
time: float
|
|
91
|
+
cumulative_time: float
|
|
92
|
+
memory: int = 0
|
|
93
|
+
per_call_time: float = 0.0
|
|
94
|
+
per_call_memory: int = 0
|
|
95
|
+
filename: str = ""
|
|
96
|
+
lineno: int = 0
|
|
97
|
+
|
|
98
|
+
def __post_init__(self) -> None:
|
|
99
|
+
"""Calculate derived stats."""
|
|
100
|
+
if self.calls > 0:
|
|
101
|
+
self.per_call_time = self.time / self.calls
|
|
102
|
+
if self.memory > 0:
|
|
103
|
+
self.per_call_memory = self.memory // self.calls
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ProfilingResult:
|
|
108
|
+
"""Complete profiling results.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
function_stats: Statistics for all profiled functions
|
|
112
|
+
hotspots: Performance bottlenecks (top N slowest functions)
|
|
113
|
+
memory_stats: Memory usage statistics
|
|
114
|
+
call_graph: Function call hierarchy
|
|
115
|
+
total_time: Total execution time (seconds)
|
|
116
|
+
peak_memory: Peak memory usage (bytes)
|
|
117
|
+
mode: Profiling mode used
|
|
118
|
+
metadata: Additional metadata (timestamp, environment, etc.)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
function_stats: dict[str, FunctionStats]
|
|
122
|
+
hotspots: list[HotspotDict]
|
|
123
|
+
memory_stats: dict[str, Any]
|
|
124
|
+
call_graph: CallGraphDict
|
|
125
|
+
total_time: float
|
|
126
|
+
peak_memory: int
|
|
127
|
+
mode: ProfilingMode
|
|
128
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
129
|
+
|
|
130
|
+
def summary(self) -> str:
|
|
131
|
+
"""Generate text summary of profiling results.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Human-readable summary string
|
|
135
|
+
"""
|
|
136
|
+
lines = [
|
|
137
|
+
"=" * 80,
|
|
138
|
+
"Performance Profiling Report",
|
|
139
|
+
"=" * 80,
|
|
140
|
+
f"Mode: {self.mode.value}",
|
|
141
|
+
f"Total Time: {self.total_time:.4f}s",
|
|
142
|
+
f"Peak Memory: {self._format_bytes(self.peak_memory)}",
|
|
143
|
+
f"Functions Profiled: {len(self.function_stats)}",
|
|
144
|
+
"",
|
|
145
|
+
"Top 10 Hotspots (by cumulative time):",
|
|
146
|
+
"-" * 80,
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# Add hotspots
|
|
150
|
+
for i, hotspot in enumerate(self.hotspots[:10], 1):
|
|
151
|
+
func_name = hotspot["function"]
|
|
152
|
+
cum_time = hotspot["cumulative_time"]
|
|
153
|
+
calls = hotspot["calls"]
|
|
154
|
+
pct = hotspot["percent_time"]
|
|
155
|
+
|
|
156
|
+
lines.append(f"{i:2d}. {func_name:50s} {cum_time:8.4f}s ({calls:6d} calls) {pct:5.1f}%")
|
|
157
|
+
|
|
158
|
+
if self.mode == ProfilingMode.MEMORY or self.mode == ProfilingMode.FULL:
|
|
159
|
+
lines.extend(
|
|
160
|
+
[
|
|
161
|
+
"",
|
|
162
|
+
"Memory Statistics:",
|
|
163
|
+
"-" * 80,
|
|
164
|
+
f" Peak Usage: {self._format_bytes(self.memory_stats.get('peak', 0))}",
|
|
165
|
+
f" Current Usage: {self._format_bytes(self.memory_stats.get('current', 0))}",
|
|
166
|
+
f" Allocations: {self.memory_stats.get('allocations', 0):,}",
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
lines.append("=" * 80)
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
def export_json(self, filepath: str | Path) -> None:
|
|
174
|
+
"""Export profiling results to JSON file.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
filepath: Output file path
|
|
178
|
+
"""
|
|
179
|
+
filepath = Path(filepath)
|
|
180
|
+
|
|
181
|
+
# Convert to serializable format
|
|
182
|
+
data = {
|
|
183
|
+
"function_stats": {name: asdict(stats) for name, stats in self.function_stats.items()},
|
|
184
|
+
"hotspots": self.hotspots,
|
|
185
|
+
"memory_stats": self.memory_stats,
|
|
186
|
+
"call_graph": self.call_graph,
|
|
187
|
+
"total_time": self.total_time,
|
|
188
|
+
"peak_memory": self.peak_memory,
|
|
189
|
+
"mode": self.mode.value,
|
|
190
|
+
"metadata": self.metadata,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
with filepath.open("w") as f:
|
|
194
|
+
json.dump(data, f, indent=2)
|
|
195
|
+
|
|
196
|
+
logger.info(f"Profiling results exported to {filepath}")
|
|
197
|
+
|
|
198
|
+
def export_html(self, filepath: str | Path) -> None:
|
|
199
|
+
"""Export profiling results to HTML file with flame graph.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
filepath: Output file path
|
|
203
|
+
"""
|
|
204
|
+
filepath = Path(filepath)
|
|
205
|
+
|
|
206
|
+
html = self._generate_html()
|
|
207
|
+
|
|
208
|
+
with filepath.open("w") as f:
|
|
209
|
+
f.write(html)
|
|
210
|
+
|
|
211
|
+
logger.info(f"HTML report exported to {filepath}")
|
|
212
|
+
|
|
213
|
+
def export_text(self, filepath: str | Path) -> None:
|
|
214
|
+
"""Export profiling results to text file.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
filepath: Output file path
|
|
218
|
+
"""
|
|
219
|
+
filepath = Path(filepath)
|
|
220
|
+
|
|
221
|
+
with filepath.open("w") as f:
|
|
222
|
+
f.write(self.summary())
|
|
223
|
+
|
|
224
|
+
logger.info(f"Text report exported to {filepath}")
|
|
225
|
+
|
|
226
|
+
def _format_bytes(self, bytes_value: int) -> str:
|
|
227
|
+
"""Format bytes as human-readable string.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
bytes_value: Number of bytes
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Formatted string (e.g., "10.5 MB")
|
|
234
|
+
"""
|
|
235
|
+
if bytes_value == 0:
|
|
236
|
+
return "0 B"
|
|
237
|
+
|
|
238
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
239
|
+
unit_index = 0
|
|
240
|
+
value = float(bytes_value)
|
|
241
|
+
|
|
242
|
+
while value >= 1024.0 and unit_index < len(units) - 1:
|
|
243
|
+
value /= 1024.0
|
|
244
|
+
unit_index += 1
|
|
245
|
+
|
|
246
|
+
return f"{value:.2f} {units[unit_index]}"
|
|
247
|
+
|
|
248
|
+
def _generate_html(self) -> str:
|
|
249
|
+
"""Generate HTML report with styling.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
HTML string
|
|
253
|
+
"""
|
|
254
|
+
html = f"""<!DOCTYPE html>
|
|
255
|
+
<html>
|
|
256
|
+
<head>
|
|
257
|
+
<title>Performance Profiling Report</title>
|
|
258
|
+
<style>
|
|
259
|
+
body {{ font-family: 'Courier New', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }}
|
|
260
|
+
h1 {{ color: #4ec9b0; }}
|
|
261
|
+
h2 {{ color: #569cd6; margin-top: 30px; }}
|
|
262
|
+
table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
|
|
263
|
+
th {{ background: #2d2d30; color: #4ec9b0; padding: 10px; text-align: left; }}
|
|
264
|
+
td {{ padding: 8px; border-bottom: 1px solid #3e3e42; }}
|
|
265
|
+
.hotspot {{ background: #3c2929; }}
|
|
266
|
+
.stats {{ margin: 10px 0; padding: 10px; background: #2d2d30; border-radius: 5px; }}
|
|
267
|
+
.metric {{ display: inline-block; margin: 10px 20px 10px 0; }}
|
|
268
|
+
.value {{ color: #ce9178; font-weight: bold; }}
|
|
269
|
+
</style>
|
|
270
|
+
</head>
|
|
271
|
+
<body>
|
|
272
|
+
<h1>Performance Profiling Report</h1>
|
|
273
|
+
|
|
274
|
+
<div class="stats">
|
|
275
|
+
<div class="metric">Mode: <span class="value">{self.mode.value}</span></div>
|
|
276
|
+
<div class="metric">Total Time: <span class="value">{self.total_time:.4f}s</span></div>
|
|
277
|
+
<div class="metric">Peak Memory: <span class="value">{self._format_bytes(self.peak_memory)}</span></div>
|
|
278
|
+
<div class="metric">Functions: <span class="value">{len(self.function_stats)}</span></div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<h2>Top Hotspots</h2>
|
|
282
|
+
<table>
|
|
283
|
+
<tr>
|
|
284
|
+
<th>Rank</th>
|
|
285
|
+
<th>Function</th>
|
|
286
|
+
<th>Cumulative Time</th>
|
|
287
|
+
<th>Calls</th>
|
|
288
|
+
<th>% of Total</th>
|
|
289
|
+
</tr>
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
for i, hotspot in enumerate(self.hotspots[:20], 1):
|
|
293
|
+
css_class = "hotspot" if i <= 5 else ""
|
|
294
|
+
html += f""" <tr class="{css_class}">
|
|
295
|
+
<td>{i}</td>
|
|
296
|
+
<td>{hotspot["function"]}</td>
|
|
297
|
+
<td>{hotspot["cumulative_time"]:.4f}s</td>
|
|
298
|
+
<td>{hotspot["calls"]}</td>
|
|
299
|
+
<td>{hotspot["percent_time"]:.1f}%</td>
|
|
300
|
+
</tr>
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
html += """ </table>
|
|
304
|
+
</body>
|
|
305
|
+
</html>
|
|
306
|
+
"""
|
|
307
|
+
return html
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class PerformanceProfiler:
|
|
311
|
+
"""Comprehensive performance profiler for analysis workflows.
|
|
312
|
+
|
|
313
|
+
Profiles CPU usage, memory consumption, I/O operations, and identifies
|
|
314
|
+
performance bottlenecks using multiple profiling backends.
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
Basic usage::
|
|
318
|
+
|
|
319
|
+
profiler = PerformanceProfiler()
|
|
320
|
+
profiler.start()
|
|
321
|
+
# Run analysis code
|
|
322
|
+
result = profiler.stop()
|
|
323
|
+
print(result.summary())
|
|
324
|
+
|
|
325
|
+
Context manager::
|
|
326
|
+
|
|
327
|
+
with PerformanceProfiler() as profiler:
|
|
328
|
+
# Run analysis code
|
|
329
|
+
pass
|
|
330
|
+
result = profiler.get_results()
|
|
331
|
+
|
|
332
|
+
Decorator::
|
|
333
|
+
|
|
334
|
+
@PerformanceProfiler.profile_function()
|
|
335
|
+
def my_analysis_function(data):
|
|
336
|
+
# Analysis code
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
References:
|
|
340
|
+
Python cProfile: https://docs.python.org/3/library/profile.html
|
|
341
|
+
tracemalloc: https://docs.python.org/3/library/tracemalloc.html
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
def __init__(self, mode: ProfilingMode = ProfilingMode.FUNCTION) -> None:
|
|
345
|
+
"""Initialize profiler.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
mode: Profiling mode (FUNCTION, LINE, MEMORY, IO, FULL)
|
|
349
|
+
"""
|
|
350
|
+
self.mode = mode
|
|
351
|
+
self._profiler: cProfile.Profile | None = None
|
|
352
|
+
self._start_time: float = 0.0
|
|
353
|
+
self._end_time: float = 0.0
|
|
354
|
+
self._memory_start: tuple[int, int] = (0, 0)
|
|
355
|
+
self._memory_peak: int = 0
|
|
356
|
+
self._result: ProfilingResult | None = None
|
|
357
|
+
self._is_running: bool = False
|
|
358
|
+
|
|
359
|
+
# Check optional dependencies
|
|
360
|
+
self._line_profiler_available = self._check_line_profiler()
|
|
361
|
+
self._memory_profiler_available = self._check_memory_profiler()
|
|
362
|
+
|
|
363
|
+
def _check_line_profiler(self) -> bool:
|
|
364
|
+
"""Check if line_profiler is available.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
True if line_profiler is installed
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
import line_profiler # type: ignore[import-not-found] # noqa: F401
|
|
371
|
+
|
|
372
|
+
return True
|
|
373
|
+
except ImportError:
|
|
374
|
+
if self.mode == ProfilingMode.LINE:
|
|
375
|
+
logger.warning(
|
|
376
|
+
"line_profiler not installed. Install with: pip install line_profiler"
|
|
377
|
+
)
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def _check_memory_profiler(self) -> bool:
|
|
381
|
+
"""Check if memory_profiler is available.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
True if memory_profiler is installed
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
import memory_profiler # type: ignore[import-not-found] # noqa: F401
|
|
388
|
+
|
|
389
|
+
return True
|
|
390
|
+
except ImportError:
|
|
391
|
+
if self.mode == ProfilingMode.MEMORY:
|
|
392
|
+
logger.info("memory_profiler not installed (optional). Using tracemalloc instead.")
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
def start(self) -> None:
|
|
396
|
+
"""Start profiling."""
|
|
397
|
+
if self._is_running:
|
|
398
|
+
logger.warning("Profiler is already running")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
self._start_time = time.perf_counter()
|
|
402
|
+
self._is_running = True
|
|
403
|
+
|
|
404
|
+
# Start appropriate profiling backend
|
|
405
|
+
if self.mode in (ProfilingMode.FUNCTION, ProfilingMode.FULL):
|
|
406
|
+
self._profiler = cProfile.Profile()
|
|
407
|
+
try:
|
|
408
|
+
self._profiler.enable()
|
|
409
|
+
except ValueError as e:
|
|
410
|
+
# Handle nested profiler case
|
|
411
|
+
if "Another profiling tool is already active" in str(e):
|
|
412
|
+
logger.warning(
|
|
413
|
+
"Another profiler is active. This profiler will not collect detailed statistics."
|
|
414
|
+
)
|
|
415
|
+
self._profiler = None
|
|
416
|
+
else:
|
|
417
|
+
raise
|
|
418
|
+
|
|
419
|
+
if self.mode in (ProfilingMode.MEMORY, ProfilingMode.FULL):
|
|
420
|
+
if not tracemalloc.is_tracing():
|
|
421
|
+
tracemalloc.start()
|
|
422
|
+
self._memory_start = tracemalloc.get_traced_memory()
|
|
423
|
+
|
|
424
|
+
logger.info(f"Profiling started in {self.mode.value} mode")
|
|
425
|
+
|
|
426
|
+
def stop(self) -> ProfilingResult:
|
|
427
|
+
"""Stop profiling and generate results.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Profiling results
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
RuntimeError: If profiler is not running
|
|
434
|
+
"""
|
|
435
|
+
if not self._is_running:
|
|
436
|
+
raise RuntimeError("Profiler is not running. Call start() first.")
|
|
437
|
+
|
|
438
|
+
self._end_time = time.perf_counter()
|
|
439
|
+
total_time = self._end_time - self._start_time
|
|
440
|
+
|
|
441
|
+
# Stop profiling backends
|
|
442
|
+
if self._profiler is not None:
|
|
443
|
+
self._profiler.disable()
|
|
444
|
+
|
|
445
|
+
memory_stats: dict[str, Any] = {}
|
|
446
|
+
if self.mode in (ProfilingMode.MEMORY, ProfilingMode.FULL):
|
|
447
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
448
|
+
self._memory_peak = peak
|
|
449
|
+
memory_stats = {
|
|
450
|
+
"current": current,
|
|
451
|
+
"peak": peak,
|
|
452
|
+
"allocations": tracemalloc.get_tracemalloc_memory(),
|
|
453
|
+
}
|
|
454
|
+
tracemalloc.stop()
|
|
455
|
+
|
|
456
|
+
# Extract statistics
|
|
457
|
+
function_stats = self._extract_function_stats()
|
|
458
|
+
hotspots = self._identify_hotspots(function_stats, total_time)
|
|
459
|
+
call_graph = self._build_call_graph()
|
|
460
|
+
|
|
461
|
+
# Create result
|
|
462
|
+
self._result = ProfilingResult(
|
|
463
|
+
function_stats=function_stats,
|
|
464
|
+
hotspots=hotspots,
|
|
465
|
+
memory_stats=memory_stats,
|
|
466
|
+
call_graph=call_graph,
|
|
467
|
+
total_time=total_time,
|
|
468
|
+
peak_memory=self._memory_peak,
|
|
469
|
+
mode=self.mode,
|
|
470
|
+
metadata={
|
|
471
|
+
"python_version": sys.version,
|
|
472
|
+
"platform": sys.platform,
|
|
473
|
+
"timestamp": time.time(),
|
|
474
|
+
},
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
self._is_running = False
|
|
478
|
+
logger.info(f"Profiling stopped. Total time: {total_time:.4f}s")
|
|
479
|
+
|
|
480
|
+
return self._result
|
|
481
|
+
|
|
482
|
+
def get_results(self) -> ProfilingResult | None:
|
|
483
|
+
"""Get profiling results without stopping.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Profiling results or None if not available
|
|
487
|
+
"""
|
|
488
|
+
return self._result
|
|
489
|
+
|
|
490
|
+
@contextmanager
|
|
491
|
+
def profile(self) -> Iterator[PerformanceProfiler]:
|
|
492
|
+
"""Context manager for profiling code blocks.
|
|
493
|
+
|
|
494
|
+
Yields:
|
|
495
|
+
PerformanceProfiler instance
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
>>> with PerformanceProfiler() as profiler:
|
|
499
|
+
... # Code to profile
|
|
500
|
+
... pass
|
|
501
|
+
>>> result = profiler.get_results()
|
|
502
|
+
"""
|
|
503
|
+
self.start()
|
|
504
|
+
try:
|
|
505
|
+
yield self
|
|
506
|
+
finally:
|
|
507
|
+
self.stop()
|
|
508
|
+
|
|
509
|
+
def __enter__(self) -> PerformanceProfiler:
|
|
510
|
+
"""Enter context manager."""
|
|
511
|
+
self.start()
|
|
512
|
+
return self
|
|
513
|
+
|
|
514
|
+
def __exit__(self, exc_type: type, exc_val: Exception, exc_tb: Any) -> None:
|
|
515
|
+
"""Exit context manager."""
|
|
516
|
+
if self._is_running:
|
|
517
|
+
self.stop()
|
|
518
|
+
|
|
519
|
+
@staticmethod
|
|
520
|
+
def profile_function(
|
|
521
|
+
mode: ProfilingMode = ProfilingMode.FUNCTION,
|
|
522
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
523
|
+
"""Decorator for profiling individual functions.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
mode: Profiling mode
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Decorator function
|
|
530
|
+
|
|
531
|
+
Example:
|
|
532
|
+
>>> @PerformanceProfiler.profile_function()
|
|
533
|
+
>>> def my_function(x):
|
|
534
|
+
... return x * 2
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
538
|
+
@functools.wraps(func)
|
|
539
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
540
|
+
profiler = PerformanceProfiler(mode=mode)
|
|
541
|
+
profiler.start()
|
|
542
|
+
try:
|
|
543
|
+
result = func(*args, **kwargs)
|
|
544
|
+
return result
|
|
545
|
+
finally:
|
|
546
|
+
profiling_result = profiler.stop()
|
|
547
|
+
logger.info(
|
|
548
|
+
f"\nProfiling results for {func.__name__}:\n{profiling_result.summary()}"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return wrapper
|
|
552
|
+
|
|
553
|
+
return decorator
|
|
554
|
+
|
|
555
|
+
def _extract_function_stats(self) -> dict[str, FunctionStats]:
|
|
556
|
+
"""Extract function statistics from profiler.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Dictionary mapping function names to their statistics
|
|
560
|
+
"""
|
|
561
|
+
if self._profiler is None:
|
|
562
|
+
return {}
|
|
563
|
+
|
|
564
|
+
# Get statistics from cProfile
|
|
565
|
+
stats_stream = io.StringIO()
|
|
566
|
+
stats = pstats.Stats(self._profiler, stream=stats_stream)
|
|
567
|
+
stats.sort_stats(pstats.SortKey.CUMULATIVE)
|
|
568
|
+
|
|
569
|
+
function_stats: dict[str, FunctionStats] = {}
|
|
570
|
+
|
|
571
|
+
for func_key, (_cc, nc, tt, ct, _callers) in stats.stats.items(): # type: ignore[attr-defined]
|
|
572
|
+
filename, lineno, func_name = func_key
|
|
573
|
+
|
|
574
|
+
# Create readable function name
|
|
575
|
+
if filename == "~":
|
|
576
|
+
full_name = func_name
|
|
577
|
+
else:
|
|
578
|
+
full_name = f"{Path(filename).name}:{func_name}"
|
|
579
|
+
|
|
580
|
+
function_stats[full_name] = FunctionStats(
|
|
581
|
+
name=full_name,
|
|
582
|
+
calls=nc,
|
|
583
|
+
time=tt,
|
|
584
|
+
cumulative_time=ct,
|
|
585
|
+
filename=filename,
|
|
586
|
+
lineno=lineno,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return function_stats
|
|
590
|
+
|
|
591
|
+
def _identify_hotspots(
|
|
592
|
+
self, function_stats: dict[str, FunctionStats], total_time: float
|
|
593
|
+
) -> list[HotspotDict]:
|
|
594
|
+
"""Identify performance hotspots.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
function_stats: Function statistics
|
|
598
|
+
total_time: Total execution time
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
List of hotspot dictionaries sorted by cumulative time
|
|
602
|
+
"""
|
|
603
|
+
hotspots: list[HotspotDict] = []
|
|
604
|
+
|
|
605
|
+
for func_name, stats in function_stats.items():
|
|
606
|
+
percent_time = (stats.cumulative_time / total_time * 100.0) if total_time > 0 else 0.0
|
|
607
|
+
|
|
608
|
+
hotspots.append(
|
|
609
|
+
{
|
|
610
|
+
"function": func_name,
|
|
611
|
+
"calls": stats.calls,
|
|
612
|
+
"time": stats.time,
|
|
613
|
+
"cumulative_time": stats.cumulative_time,
|
|
614
|
+
"percent_time": percent_time,
|
|
615
|
+
"per_call_time": stats.per_call_time,
|
|
616
|
+
}
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Sort by cumulative time (descending)
|
|
620
|
+
hotspots.sort(key=lambda x: x["cumulative_time"], reverse=True)
|
|
621
|
+
|
|
622
|
+
return hotspots
|
|
623
|
+
|
|
624
|
+
def _build_call_graph(self) -> CallGraphDict:
|
|
625
|
+
"""Build function call graph.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
Dictionary mapping functions to list of called functions
|
|
629
|
+
"""
|
|
630
|
+
if self._profiler is None:
|
|
631
|
+
return {}
|
|
632
|
+
|
|
633
|
+
call_graph: CallGraphDict = {}
|
|
634
|
+
|
|
635
|
+
stats = pstats.Stats(self._profiler)
|
|
636
|
+
|
|
637
|
+
for func_key in stats.stats: # type: ignore[attr-defined]
|
|
638
|
+
filename, lineno, func_name = func_key
|
|
639
|
+
|
|
640
|
+
# Create readable function name
|
|
641
|
+
if filename == "~":
|
|
642
|
+
full_name = func_name
|
|
643
|
+
else:
|
|
644
|
+
full_name = f"{Path(filename).name}:{func_name}"
|
|
645
|
+
|
|
646
|
+
# Get callees (functions called by this function)
|
|
647
|
+
# Note: all_callees may not be available in all Python versions
|
|
648
|
+
callees: list[str] = []
|
|
649
|
+
all_callees = getattr(stats, "all_callees", None)
|
|
650
|
+
if all_callees is not None and func_key in all_callees:
|
|
651
|
+
for callee_key in all_callees[func_key]:
|
|
652
|
+
callee_filename, callee_lineno, callee_name = callee_key
|
|
653
|
+
if callee_filename == "~":
|
|
654
|
+
callee_full = callee_name
|
|
655
|
+
else:
|
|
656
|
+
callee_full = f"{Path(callee_filename).name}:{callee_name}"
|
|
657
|
+
callees.append(callee_full)
|
|
658
|
+
|
|
659
|
+
call_graph[full_name] = callees
|
|
660
|
+
|
|
661
|
+
return call_graph
|
|
@@ -8,7 +8,7 @@ from collections.abc import Callable
|
|
|
8
8
|
from functools import reduce, wraps
|
|
9
9
|
from typing import Any, TypeVar
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from oscura.core.types import WaveformTrace
|
|
12
12
|
|
|
13
13
|
# Type variables for generic composition
|
|
14
14
|
T = TypeVar("T")
|
|
@@ -60,8 +60,16 @@ def compose(*funcs: TraceFunc) -> TraceFunc:
|
|
|
60
60
|
# Apply functions in reverse order (right to left)
|
|
61
61
|
return reduce(lambda val, func: func(val), reversed(funcs), x)
|
|
62
62
|
|
|
63
|
-
# Preserve function metadata
|
|
64
|
-
|
|
63
|
+
# Preserve function metadata (handle functools.partial which lacks __name__)
|
|
64
|
+
func_names = []
|
|
65
|
+
for f in funcs:
|
|
66
|
+
if hasattr(f, "__name__"):
|
|
67
|
+
func_names.append(f.__name__)
|
|
68
|
+
elif hasattr(f, "func"): # functools.partial
|
|
69
|
+
func_names.append(f.func.__name__)
|
|
70
|
+
else:
|
|
71
|
+
func_names.append(repr(f))
|
|
72
|
+
composed.__name__ = "compose(" + ", ".join(func_names) + ")"
|
|
65
73
|
composed.__doc__ = f"Composition of {len(funcs)} functions"
|
|
66
74
|
|
|
67
75
|
return composed
|