@trillboards/edge-sdk 0.2.140 → 0.2.150
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.
- package/README.md +2 -2
- package/deploy/detector/pyproject.toml +20 -2
- package/deploy/detector/tests/test_execution.py +124 -0
- package/deploy/detector/tests/test_model_identity.py +80 -0
- package/deploy/detector/tests/test_registry.py +18 -0
- package/deploy/detector/tests/test_server.py +3 -1
- package/deploy/detector/tests/test_version_stamp.py +4 -1
- package/deploy/detector/trillboards_detector/_version.py +2 -2
- package/deploy/detector/trillboards_detector/cli.py +3 -0
- package/deploy/detector/trillboards_detector/detections.py +7 -0
- package/deploy/detector/trillboards_detector/errors.py +4 -0
- package/deploy/detector/trillboards_detector/execution.py +109 -0
- package/deploy/detector/trillboards_detector/loader.py +6 -3
- package/deploy/detector/trillboards_detector/registry.py +27 -5
- package/deploy/detector/trillboards_detector/server.py +29 -2
- package/deploy/detector/trillboards_detector/session.py +15 -3
- package/deploy/litertlm/test_trillboards_litertlm_shim.py +81 -0
- package/deploy/litertlm/test_verify_model_proof.py +30 -0
- package/deploy/litertlm/trillboards_litertlm_shim.py +150 -1
- package/deploy/litertlm/verify_model_proof.py +30 -0
- package/deploy/macos/build-macos-pkg.sh +10 -10
- package/deploy/macos/scripts/setup-detector.sh +2 -2
- package/deploy/macos/scripts/setup-litertlm.sh +66 -30
- package/deploy/macos/test-setup-litertlm-reinstall.sh +57 -4
- package/deploy/systemd/setup-detector.sh +2 -2
- package/deploy/systemd/setup-litertlm.sh +51 -27
- package/deploy/windows/build-windows.ps1 +10 -6
- package/deploy/windows/bundle-windows-installer.ps1 +3 -3
- package/deploy/windows/e2e/test-local-sense.ps1 +6 -5
- package/deploy/windows/install-update.ps1 +52 -9
- package/deploy/windows/set-executable-icon.ps1 +24 -5
- package/deploy/windows/setup-detector.ps1 +142 -4
- package/deploy/windows/setup-litertlm.ps1 +53 -10
- package/dist/AudienceAnalyzeClient.d.ts +10 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
- package/dist/AudienceAnalyzeClient.js +5 -4
- package/dist/AudienceAnalyzeClient.js.map +1 -1
- package/dist/CloudSensingManager.d.ts +38 -1
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +403 -25
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/DetectorFieldProducer.d.ts +5 -1
- package/dist/DetectorFieldProducer.d.ts.map +1 -1
- package/dist/DetectorFieldProducer.js +8 -1
- package/dist/DetectorFieldProducer.js.map +1 -1
- package/dist/DurableVlmCandidateSpool.d.ts +83 -2
- package/dist/DurableVlmCandidateSpool.d.ts.map +1 -1
- package/dist/DurableVlmCandidateSpool.js +596 -30
- package/dist/DurableVlmCandidateSpool.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/FrameGroupingShadow.d.ts +51 -0
- package/dist/FrameGroupingShadow.d.ts.map +1 -0
- package/dist/FrameGroupingShadow.js +158 -0
- package/dist/FrameGroupingShadow.js.map +1 -0
- package/dist/ImageClassifierFieldProducer.d.ts +36 -0
- package/dist/ImageClassifierFieldProducer.d.ts.map +1 -0
- package/dist/ImageClassifierFieldProducer.js +150 -0
- package/dist/ImageClassifierFieldProducer.js.map +1 -0
- package/dist/LiteRtVlmProvider.d.ts +5 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js +72 -7
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/LocalDetectorClient.d.ts +6 -0
- package/dist/LocalDetectorClient.d.ts.map +1 -1
- package/dist/LocalDetectorClient.js +8 -0
- package/dist/LocalDetectorClient.js.map +1 -1
- package/dist/OrderedFrameReview.d.ts +57 -0
- package/dist/OrderedFrameReview.d.ts.map +1 -0
- package/dist/OrderedFrameReview.js +121 -0
- package/dist/OrderedFrameReview.js.map +1 -0
- package/dist/OrderedReviewClient.d.ts +38 -0
- package/dist/OrderedReviewClient.d.ts.map +1 -0
- package/dist/OrderedReviewClient.js +121 -0
- package/dist/OrderedReviewClient.js.map +1 -0
- package/dist/OrderedSequenceStore.d.ts +69 -0
- package/dist/OrderedSequenceStore.d.ts.map +1 -0
- package/dist/OrderedSequenceStore.js +257 -0
- package/dist/OrderedSequenceStore.js.map +1 -0
- package/dist/ProfileReviewWork.d.ts +68 -0
- package/dist/ProfileReviewWork.d.ts.map +1 -0
- package/dist/ProfileReviewWork.js +150 -0
- package/dist/ProfileReviewWork.js.map +1 -0
- package/dist/RetainedFieldContract.d.ts +21 -0
- package/dist/RetainedFieldContract.d.ts.map +1 -0
- package/dist/RetainedFieldContract.js +92 -0
- package/dist/RetainedFieldContract.js.map +1 -0
- package/dist/ReviewModelUse.d.ts +7 -0
- package/dist/ReviewModelUse.d.ts.map +1 -0
- package/dist/ReviewModelUse.js +30 -0
- package/dist/ReviewModelUse.js.map +1 -0
- package/dist/ReviewPriority.d.ts +12 -0
- package/dist/ReviewPriority.d.ts.map +1 -0
- package/dist/ReviewPriority.js +45 -0
- package/dist/ReviewPriority.js.map +1 -0
- package/dist/RuntimeModelSelection.d.ts +10 -0
- package/dist/RuntimeModelSelection.d.ts.map +1 -0
- package/dist/RuntimeModelSelection.js +58 -0
- package/dist/RuntimeModelSelection.js.map +1 -0
- package/dist/SceneChangeDetector.d.ts +1 -1
- package/dist/SceneChangeDetector.d.ts.map +1 -1
- package/dist/SceneChangeDetector.js +18 -6
- package/dist/SceneChangeDetector.js.map +1 -1
- package/dist/ScopedVlmReview.d.ts +76 -0
- package/dist/ScopedVlmReview.d.ts.map +1 -0
- package/dist/ScopedVlmReview.js +267 -0
- package/dist/ScopedVlmReview.js.map +1 -0
- package/dist/SensingProfileClient.d.ts +50 -0
- package/dist/SensingProfileClient.d.ts.map +1 -1
- package/dist/SensingProfileClient.js +58 -2
- package/dist/SensingProfileClient.js.map +1 -1
- package/dist/TemporalReviewClient.d.ts +43 -0
- package/dist/TemporalReviewClient.d.ts.map +1 -0
- package/dist/TemporalReviewClient.js +117 -0
- package/dist/TemporalReviewClient.js.map +1 -0
- package/dist/TemporalReviewJournal.d.ts +91 -0
- package/dist/TemporalReviewJournal.d.ts.map +1 -0
- package/dist/TemporalReviewJournal.js +337 -0
- package/dist/TemporalReviewJournal.js.map +1 -0
- package/dist/UrgentReviewTriage.d.ts +35 -0
- package/dist/UrgentReviewTriage.d.ts.map +1 -0
- package/dist/UrgentReviewTriage.js +52 -0
- package/dist/UrgentReviewTriage.js.map +1 -0
- package/dist/VideoSegmentUploader.d.ts +293 -0
- package/dist/VideoSegmentUploader.d.ts.map +1 -0
- package/dist/VideoSegmentUploader.js +1002 -0
- package/dist/VideoSegmentUploader.js.map +1 -0
- package/dist/VlmCandidateBuffer.d.ts +19 -0
- package/dist/VlmCandidateBuffer.d.ts.map +1 -1
- package/dist/VlmCandidateBuffer.js.map +1 -1
- package/dist/autoSense.d.ts +2 -2
- package/dist/autoSense.d.ts.map +1 -1
- package/dist/autoSense.js +16 -11
- package/dist/autoSense.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/continuousReview.d.ts +71 -0
- package/dist/continuousReview.d.ts.map +1 -0
- package/dist/continuousReview.js +53 -0
- package/dist/continuousReview.js.map +1 -0
- package/dist/detectorCountReview.d.ts +9 -0
- package/dist/detectorCountReview.d.ts.map +1 -0
- package/dist/detectorCountReview.js +79 -0
- package/dist/detectorCountReview.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/installerOnboardingMode.d.ts.map +1 -1
- package/dist/installerOnboardingMode.js +5 -3
- package/dist/installerOnboardingMode.js.map +1 -1
- package/dist/localInferenceSchema.d.ts +8 -0
- package/dist/localInferenceSchema.d.ts.map +1 -1
- package/dist/localInferenceSchema.js +3 -3
- package/dist/localInferenceSchema.js.map +1 -1
- package/dist/localSenseE2eHarness.d.ts +12 -0
- package/dist/localSenseE2eHarness.d.ts.map +1 -1
- package/dist/localSenseE2eHarness.js +77 -4
- package/dist/localSenseE2eHarness.js.map +1 -1
- package/dist/localSensing.d.ts +91 -18
- package/dist/localSensing.d.ts.map +1 -1
- package/dist/localSensing.js +678 -153
- package/dist/localSensing.js.map +1 -1
- package/dist/mixedScopedCounts.d.ts +17 -0
- package/dist/mixedScopedCounts.d.ts.map +1 -0
- package/dist/mixedScopedCounts.js +90 -0
- package/dist/mixedScopedCounts.js.map +1 -0
- package/dist/sensingCatchRate.d.ts +9 -5
- package/dist/sensingCatchRate.d.ts.map +1 -1
- package/dist/sensingCatchRate.js +10 -6
- package/dist/sensingCatchRate.js.map +1 -1
- package/dist/sensingRunMode.d.ts +5 -0
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +56 -0
- package/dist/sensingRunMode.js.map +1 -1
- package/package.json +7 -4
- package/retained/ArchiveLifecycle.cjs +79 -0
- package/retained/ModelStageScheduler.cjs +66 -0
- package/retained/ProfileAuthorityClient.cjs +88 -0
- package/retained/RetainedCaptureClient.cjs +164 -0
- package/retained/WaitRecheckCaptureClient.cjs +109 -0
- package/retained/__tests__/archive-lifecycle.test.cjs +117 -0
- package/retained/__tests__/runtime.test.cjs +384 -0
- package/retained/alert-rule-policy.cjs +37 -0
- package/retained/manager.cjs +133 -0
- package/retained/pilot-profile-client.cjs +82 -0
- package/retained/temporal-grouping.cjs +78 -0
- package/retained/website-api-capability.cjs +122 -0
package/README.md
CHANGED
|
@@ -16,11 +16,11 @@ The Edge SDK runs on digital signage hardware (kiosks, vending machines, billboa
|
|
|
16
16
|
|
|
17
17
|
- **Audience sensing** — on-device face detection, gaze tracking, emotion analysis, foot traffic estimation via ONNX models (no cloud dependency)
|
|
18
18
|
- **Ad delivery** — Chromium browser lifecycle, display management, VAST/VPAID ad rendering
|
|
19
|
-
- **Federated learning** —
|
|
19
|
+
- **Federated learning** — on-device model training with sparse gradient uploads
|
|
20
20
|
- **Cloud inference** — Gemini Vision scene analysis, content moderation, contextual targeting
|
|
21
21
|
- **Device management** — heartbeat, health monitoring, auto-recovery, signal buffering for offline resilience
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Detection runs on the device, and the agent transmits the audience metrics. In local-sensing mode (the Sense Anything connector) it also sends Trillboards a still frame with each alert-worthy moment it analyses, and in cloud-sensing mode every frame it asks the cloud model to read; Trillboards keeps those frames for 400 days.
|
|
24
24
|
|
|
25
25
|
## Configuration
|
|
26
26
|
|
|
@@ -10,7 +10,7 @@ name = "trillboards-detector"
|
|
|
10
10
|
# constant, a `labels.py`-only change survives `uv tool install` AND
|
|
11
11
|
# `uv tool install --force`; move the version and a bare `uv tool install`
|
|
12
12
|
# reinstalls. See `stamp_version.py`.
|
|
13
|
-
version = "0.2.0+src.
|
|
13
|
+
version = "0.2.0+src.6d43cf061068"
|
|
14
14
|
description = "On-device object-detector service for the Trillboards BYO-CCTV connector (loopback HTTP, ONNX Runtime)."
|
|
15
15
|
requires-python = ">=3.10"
|
|
16
16
|
|
|
@@ -29,11 +29,29 @@ requires-python = ">=3.10"
|
|
|
29
29
|
# `docs/runbooks/cctv-detector-tier.md` §Runtime. A bump is a re-measurement,
|
|
30
30
|
# not a version-number edit.
|
|
31
31
|
dependencies = [
|
|
32
|
-
"onnxruntime==1.29.0",
|
|
33
32
|
"numpy==2.5.2",
|
|
34
33
|
"pillow==12.3.0",
|
|
35
34
|
]
|
|
36
35
|
|
|
36
|
+
# Exactly one runtime per managed tool environment. Both wheels export the
|
|
37
|
+
# same Python module; installing them together is not a CPU fallback strategy.
|
|
38
|
+
# Normal provisioners select [cpu], or [cuda] on a detected NVIDIA Windows host.
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
cpu = ["onnxruntime==1.29.0"]
|
|
41
|
+
cuda = [
|
|
42
|
+
"onnxruntime-gpu[cuda,cudnn]==1.29.0",
|
|
43
|
+
"nvidia-cublas==13.8.0.4",
|
|
44
|
+
"nvidia-cuda-nvrtc==13.4.92",
|
|
45
|
+
"nvidia-cuda-runtime==13.4.92",
|
|
46
|
+
"nvidia-cudnn-cu13==9.27.0.42",
|
|
47
|
+
"nvidia-cufft==12.4.0.43",
|
|
48
|
+
"nvidia-curand==10.4.4.72",
|
|
49
|
+
"nvidia-nvjitlink==13.4.92",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.uv]
|
|
53
|
+
conflicts = [[{extra = "cpu"}, {extra = "cuda"}]]
|
|
54
|
+
|
|
37
55
|
[project.scripts]
|
|
38
56
|
trillboards-detector = "trillboards_detector.cli:main"
|
|
39
57
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Provider-policy tests, not detector accuracy or simulated model evidence."""
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from trillboards_detector.execution import CPU, CUDA, LocalOrtSession
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Session:
|
|
8
|
+
def __init__(self, providers, failure=None, demote=False):
|
|
9
|
+
self.providers, self.failure, self.demote = providers, failure, demote
|
|
10
|
+
self.inputs = []
|
|
11
|
+
|
|
12
|
+
def get_providers(self):
|
|
13
|
+
return self.providers
|
|
14
|
+
|
|
15
|
+
def run(self, outputs, inputs):
|
|
16
|
+
self.inputs.append(inputs)
|
|
17
|
+
if self.failure:
|
|
18
|
+
raise RuntimeError(self.failure)
|
|
19
|
+
if self.demote:
|
|
20
|
+
self.providers = [CPU]
|
|
21
|
+
return inputs
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Runtime:
|
|
25
|
+
def __init__(self, available=(CUDA, CPU), *, init_error=False,
|
|
26
|
+
gpu_failure=None, cpu_failure=None, silent_cpu=False, demote=False):
|
|
27
|
+
self.available = list(available)
|
|
28
|
+
self.init_error, self.gpu_failure, self.cpu_failure = init_error, gpu_failure, cpu_failure
|
|
29
|
+
self.silent_cpu, self.demote = silent_cpu, demote
|
|
30
|
+
self.calls, self.preloads = [], 0
|
|
31
|
+
|
|
32
|
+
def get_available_providers(self):
|
|
33
|
+
return self.available
|
|
34
|
+
|
|
35
|
+
def preload_dlls(self, directory):
|
|
36
|
+
assert directory == ""
|
|
37
|
+
self.preloads += 1
|
|
38
|
+
|
|
39
|
+
def InferenceSession(self, model, *, sess_options, providers):
|
|
40
|
+
self.calls.append((model, sess_options, providers))
|
|
41
|
+
if CUDA in providers and self.init_error:
|
|
42
|
+
raise RuntimeError("driver unavailable")
|
|
43
|
+
gpu = CUDA in providers
|
|
44
|
+
return Session([CPU] if gpu and self.silent_cpu else providers,
|
|
45
|
+
self.gpu_failure if gpu else self.cpu_failure,
|
|
46
|
+
self.demote if gpu else False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestLocalExecution(unittest.TestCase):
|
|
50
|
+
def test_prefers_available_cuda_and_keeps_cpu_kernels(self):
|
|
51
|
+
r = Runtime()
|
|
52
|
+
s = LocalOrtSession(r, b"immutable model", object())
|
|
53
|
+
self.assertEqual(r.calls[0][2], [CUDA, CPU])
|
|
54
|
+
self.assertEqual(r.preloads, 1)
|
|
55
|
+
self.assertEqual(s.acceleration["backend"], "cuda")
|
|
56
|
+
self.assertIsNone(s.acceleration["fallback_reason"])
|
|
57
|
+
|
|
58
|
+
def test_cpu_only_host_never_attempts_cuda(self):
|
|
59
|
+
r = Runtime((CPU,))
|
|
60
|
+
s = LocalOrtSession(r, b"model", None)
|
|
61
|
+
self.assertEqual([c[2] for c in r.calls], [[CPU]])
|
|
62
|
+
self.assertEqual(s.acceleration["backend"], "cpu")
|
|
63
|
+
self.assertIn("unavailable", s.acceleration["fallback_reason"])
|
|
64
|
+
self.assertIsNone(s._model_bytes)
|
|
65
|
+
|
|
66
|
+
def test_explicit_cpu_is_the_rollback_even_on_nvidia(self):
|
|
67
|
+
r = Runtime()
|
|
68
|
+
s = LocalOrtSession(r, b"model", None, "cpu")
|
|
69
|
+
self.assertEqual([c[2] for c in r.calls], [[CPU]])
|
|
70
|
+
self.assertEqual(s.acceleration["fallback_reason"], "CPU explicitly selected")
|
|
71
|
+
|
|
72
|
+
def test_missing_driver_is_a_visible_cpu_fallback(self):
|
|
73
|
+
r = Runtime(init_error=True)
|
|
74
|
+
s = LocalOrtSession(r, b"same weights", None)
|
|
75
|
+
self.assertEqual([c[2] for c in r.calls], [[CUDA, CPU], [CPU]])
|
|
76
|
+
self.assertIn("driver unavailable", s.acceleration["fallback_reason"])
|
|
77
|
+
|
|
78
|
+
def test_ort_silent_cpu_initialization_is_not_reported_as_gpu(self):
|
|
79
|
+
s = LocalOrtSession(Runtime(silent_cpu=True), b"model", None)
|
|
80
|
+
self.assertEqual(s.acceleration["backend"], "cpu")
|
|
81
|
+
self.assertIn("CPU-only", s.acceleration["fallback_reason"])
|
|
82
|
+
|
|
83
|
+
def test_gpu_failure_retries_identical_captured_input_and_model_once(self):
|
|
84
|
+
model = b"exact original immutable weights"
|
|
85
|
+
r = Runtime(gpu_failure="out of GPU memory")
|
|
86
|
+
s = LocalOrtSession(r, model, None)
|
|
87
|
+
original = {"frame": object()}
|
|
88
|
+
self.assertIs(s.run(["detections"], original), original)
|
|
89
|
+
self.assertEqual(len(r.calls), 2)
|
|
90
|
+
self.assertIs(r.calls[1][0], model)
|
|
91
|
+
self.assertEqual(s.acceleration["backend"], "cpu")
|
|
92
|
+
self.assertIn("out of GPU memory", s.acceleration["fallback_reason"])
|
|
93
|
+
s.run(["detections"], original)
|
|
94
|
+
self.assertEqual(len(r.calls), 2)
|
|
95
|
+
self.assertIsNone(s._model_bytes)
|
|
96
|
+
|
|
97
|
+
def test_cpu_failure_propagates_not_a_false_negative(self):
|
|
98
|
+
s = LocalOrtSession(Runtime(gpu_failure="gpu failed", cpu_failure="bad input"), b"model", None)
|
|
99
|
+
with self.assertRaisesRegex(RuntimeError, "bad input"):
|
|
100
|
+
s.run([], {})
|
|
101
|
+
|
|
102
|
+
def test_ort_internal_runtime_demotion_updates_actual_backend(self):
|
|
103
|
+
s = LocalOrtSession(Runtime(demote=True), b"model", None)
|
|
104
|
+
s.run([], {})
|
|
105
|
+
self.assertEqual(s.acceleration["backend"], "cpu")
|
|
106
|
+
self.assertIn("demoted", s.acceleration["fallback_reason"])
|
|
107
|
+
|
|
108
|
+
def test_unknown_backend_is_rejected(self):
|
|
109
|
+
with self.assertRaises(ValueError):
|
|
110
|
+
LocalOrtSession(Runtime(), b"model", None, "intel")
|
|
111
|
+
|
|
112
|
+
def test_health_snapshot_is_safe_during_cpu_session_replacement(self):
|
|
113
|
+
s = LocalOrtSession(Runtime(), b"model", None)
|
|
114
|
+
s._session = None
|
|
115
|
+
s._acceleration = {"requested": "auto", "backend": "initializing_cpu",
|
|
116
|
+
"providers": [], "fallback_reason": "GPU failed"}
|
|
117
|
+
self.assertEqual(s.acceleration["backend"], "initializing_cpu")
|
|
118
|
+
external = s.acceleration
|
|
119
|
+
external["providers"].append("invented")
|
|
120
|
+
self.assertEqual(s.acceleration["providers"], [])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Selected model identity, with real registry logic and tiny fixture artifacts."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from trillboards_detector.errors import ModelVerificationError
|
|
9
|
+
from trillboards_detector.registry import ModelEntry, ModelRegistry
|
|
10
|
+
from trillboards_detector.server import WireError, parse_expected_model, build_detect_response
|
|
11
|
+
# CI discovers this as tests.test_model_identity; the narrow detector lane
|
|
12
|
+
# discovers it as a top-level module. Resolve the sibling in either context.
|
|
13
|
+
if __package__:
|
|
14
|
+
from .test_registry import GENERAL, PPE, FakeSession, MB
|
|
15
|
+
else:
|
|
16
|
+
from test_registry import GENERAL, PPE, FakeSession, MB
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelIdentityTests(unittest.TestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
22
|
+
self.addCleanup(self.tmp.cleanup)
|
|
23
|
+
self.paths = {v.model_name: Path(self.tmp.name) / (v.model_name + '.onnx') for v in (GENERAL, PPE)}
|
|
24
|
+
for name, path in self.paths.items():
|
|
25
|
+
path.write_bytes(('explicit fixture artifact ' + name).encode())
|
|
26
|
+
self.sessions = []
|
|
27
|
+
def factory(entry):
|
|
28
|
+
session = FakeSession(entry.vocabulary)
|
|
29
|
+
session.model_sha256 = hashlib.sha256(entry.path.read_bytes()).hexdigest()
|
|
30
|
+
self.sessions.append(session)
|
|
31
|
+
original = session.detect
|
|
32
|
+
def detect(*args):
|
|
33
|
+
result = original(*args)
|
|
34
|
+
result.model_hashes = {session.model_name: session.model_sha256}
|
|
35
|
+
return result
|
|
36
|
+
session.detect = detect
|
|
37
|
+
return session
|
|
38
|
+
self.registry = ModelRegistry([ModelEntry(GENERAL, self.paths['general'], pinned=True), ModelEntry(PPE, self.paths['ppe'])],
|
|
39
|
+
factory, lambda _session: None, 2048 * MB, log=lambda _line: None)
|
|
40
|
+
self.registry.refresh()
|
|
41
|
+
|
|
42
|
+
def pin(self, name):
|
|
43
|
+
return {'modelId': name, 'sha256': hashlib.sha256(self.paths[name].read_bytes()).hexdigest()}
|
|
44
|
+
|
|
45
|
+
def test_matching_pin_reports_session_hash(self):
|
|
46
|
+
batch = self.registry.detect(b'fixture image', .5, {'person'}, expected_model=self.pin('general'))
|
|
47
|
+
self.assertEqual(build_detect_response(batch)['model_hashes'], {'general': self.pin('general')['sha256']})
|
|
48
|
+
|
|
49
|
+
def test_wrong_hash_refused_before_requested_image_inference(self):
|
|
50
|
+
pin = {**self.pin('general'), 'sha256': 'f' * 64}
|
|
51
|
+
with self.assertRaises(ModelVerificationError):
|
|
52
|
+
self.registry.detect(b'fixture image', .5, {'person'}, expected_model=pin)
|
|
53
|
+
self.assertEqual(self.sessions[0].calls, [])
|
|
54
|
+
|
|
55
|
+
def test_explicit_specialist_is_not_replaced_by_general_shared_class(self):
|
|
56
|
+
batch = self.registry.detect(b'fixture image', .5, {'person'}, expected_model=self.pin('ppe'))
|
|
57
|
+
self.assertEqual(batch.models, ['ppe'])
|
|
58
|
+
self.assertEqual(self.sessions[0].calls, [])
|
|
59
|
+
|
|
60
|
+
def test_unknown_model_or_classes_are_refused(self):
|
|
61
|
+
for pin, classes in [({'modelId': 'missing', 'sha256': 'a' * 64}, {'person'}), (self.pin('general'), {'glove'})]:
|
|
62
|
+
with self.assertRaises(ModelVerificationError):
|
|
63
|
+
self.registry.detect(b'fixture image', .5, classes, expected_model=pin)
|
|
64
|
+
|
|
65
|
+
def test_disk_replacement_cannot_be_attributed_to_old_loaded_pin(self):
|
|
66
|
+
old = self.pin('general')
|
|
67
|
+
self.paths['general'].write_bytes(b'changed artifact with a different size')
|
|
68
|
+
with self.assertRaises(ModelVerificationError):
|
|
69
|
+
self.registry.detect(b'fixture image', .5, {'person'}, expected_model=old)
|
|
70
|
+
|
|
71
|
+
def test_wire_pin_must_be_explicit_and_complete(self):
|
|
72
|
+
self.assertIsNone(parse_expected_model(b'{}'))
|
|
73
|
+
self.assertEqual(parse_expected_model(json.dumps({'expected_model': self.pin('general')}).encode()), self.pin('general'))
|
|
74
|
+
for value in [None, {}, {'modelId': 'general', 'sha256': 'A' * 64}, {**self.pin('general'), 'extra': True}]:
|
|
75
|
+
with self.assertRaises(WireError):
|
|
76
|
+
parse_expected_model(json.dumps({'expected_model': value}).encode())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == '__main__':
|
|
80
|
+
unittest.main()
|
|
@@ -654,6 +654,24 @@ class TestDownIsNeverEmpty(RegistryCase):
|
|
|
654
654
|
class TestSnapshotIsCheapAndHonest(RegistryCase):
|
|
655
655
|
"""`/health` reports which models are resident and must not load one."""
|
|
656
656
|
|
|
657
|
+
def test_health_reports_actual_backend_and_cpu_fallback_reason(self):
|
|
658
|
+
self.write_artifact("general")
|
|
659
|
+
reg = self.registry([GENERAL], pinned="general")
|
|
660
|
+
reg.refresh()
|
|
661
|
+
session = self.sessions["general"]
|
|
662
|
+
session.acceleration = {"requested": "auto", "backend": "cuda",
|
|
663
|
+
"providers": ["CUDAExecutionProvider", "CPUExecutionProvider"],
|
|
664
|
+
"fallback_reason": None}
|
|
665
|
+
first = reg.snapshot()["models"]["general"]
|
|
666
|
+
self.assertEqual(first["providers"][0], "CUDAExecutionProvider")
|
|
667
|
+
session.acceleration = {"requested": "auto", "backend": "cpu",
|
|
668
|
+
"providers": ["CPUExecutionProvider"],
|
|
669
|
+
"fallback_reason": "GPU inference failed"}
|
|
670
|
+
second = reg.snapshot()["models"]["general"]
|
|
671
|
+
self.assertEqual(second["providers"], ["CPUExecutionProvider"])
|
|
672
|
+
self.assertEqual(second["acceleration"]["fallback_reason"], "GPU inference failed")
|
|
673
|
+
self.assertEqual(self.sessions["general"].calls, [])
|
|
674
|
+
|
|
657
675
|
def test_the_snapshot_loads_nothing_and_infers_nothing(self):
|
|
658
676
|
self.write_artifact("general")
|
|
659
677
|
self.write_artifact("ppe")
|
|
@@ -114,8 +114,10 @@ class ServerCase(unittest.TestCase):
|
|
|
114
114
|
# 20 ms poll: `shutdown()` blocks until the loop notices, and the
|
|
115
115
|
# 500 ms default turns a 22-test suite into 11 s of pure waiting.
|
|
116
116
|
threading.Thread(target=lambda: server.serve_forever(poll_interval=0.02), daemon=True).start()
|
|
117
|
-
|
|
117
|
+
# Cleanup runs last-in-first-out. Stop the selector thread before
|
|
118
|
+
# closing its socket, otherwise Windows emits WinError 10038.
|
|
118
119
|
self.addCleanup(server.server_close)
|
|
120
|
+
self.addCleanup(server.shutdown)
|
|
119
121
|
host, port = server.server_address[:2]
|
|
120
122
|
return f"http://{host}:{port}"
|
|
121
123
|
|
|
@@ -135,7 +135,10 @@ class TestVersionStamp(unittest.TestCase):
|
|
|
135
135
|
)
|
|
136
136
|
before = _load_stamper(root).expected_version()[2]
|
|
137
137
|
for path in list((root / "trillboards_detector").rglob("*.py")) + [root / "pyproject.toml"]:
|
|
138
|
-
|
|
138
|
+
# Start from normalized bytes: a checkout already using CRLF
|
|
139
|
+
# must not become CRCRLF, which is a different text document.
|
|
140
|
+
raw = path.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
141
|
+
path.write_bytes(raw.replace(b"\n", b"\r\n"))
|
|
139
142
|
after = _load_stamper(root).expected_version()[2]
|
|
140
143
|
self.assertEqual(before, after, "CRLF alone moved the fingerprint")
|
|
141
144
|
|
|
@@ -103,6 +103,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
103
103
|
),
|
|
104
104
|
)
|
|
105
105
|
s.add_argument("--poll-interval", type=float, default=DEFAULT_POLL_INTERVAL_S)
|
|
106
|
+
s.add_argument("--accelerator", choices=("auto", "cpu"), default="auto",
|
|
107
|
+
help="prefer compatible local NVIDIA CUDA, otherwise CPU; cpu is an explicit rollback")
|
|
106
108
|
s.add_argument("--log-file", default=None, help="also write to this file (Windows has no task stdout)")
|
|
107
109
|
s.add_argument("--verbose", action="store_true")
|
|
108
110
|
return p
|
|
@@ -157,6 +159,7 @@ def main(argv: "list[str] | None" = None) -> int:
|
|
|
157
159
|
poll_interval_s=args.poll_interval,
|
|
158
160
|
ram_budget_bytes=ram_budget_bytes,
|
|
159
161
|
log=LOG.info,
|
|
162
|
+
accelerator=args.accelerator,
|
|
160
163
|
)
|
|
161
164
|
loader.start()
|
|
162
165
|
|
|
@@ -57,6 +57,9 @@ class DetectionBatch:
|
|
|
57
57
|
#: The model(s) that produced this, in the order they ran. Exactly one
|
|
58
58
|
#: element for a batch off a single session; N for a merged one.
|
|
59
59
|
models: list[str] = field(default_factory=list)
|
|
60
|
+
# Derived from the exact bytes used to create each ONNX session, not a
|
|
61
|
+
# request pin, a catalogue entry, or a later read of the mutable model path.
|
|
62
|
+
model_hashes: dict[str, str] = field(default_factory=dict)
|
|
60
63
|
|
|
61
64
|
|
|
62
65
|
def merge_batches(batches: "list[DetectionBatch]") -> DetectionBatch:
|
|
@@ -92,6 +95,10 @@ def merge_batches(batches: "list[DetectionBatch]") -> DetectionBatch:
|
|
|
92
95
|
merged.degenerate_dropped += b.degenerate_dropped
|
|
93
96
|
merged.unnamed_class_dropped += b.unnamed_class_dropped
|
|
94
97
|
merged.models.extend(b.models)
|
|
98
|
+
for model, digest in b.model_hashes.items():
|
|
99
|
+
if model in merged.model_hashes and merged.model_hashes[model] != digest:
|
|
100
|
+
raise ValueError("one frame cannot merge different artifacts under one model id")
|
|
101
|
+
merged.model_hashes[model] = digest
|
|
95
102
|
return merged
|
|
96
103
|
|
|
97
104
|
|
|
@@ -37,6 +37,10 @@ class ModelContractError(RuntimeError):
|
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class ModelVerificationError(RuntimeError):
|
|
41
|
+
"""A selected artifact is not the one in the loaded inference session."""
|
|
42
|
+
|
|
43
|
+
|
|
40
44
|
class UnknownClasses(ValueError):
|
|
41
45
|
"""The caller named classes no resident-or-present model can emit.
|
|
42
46
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Select one local ORT backend and expose every CPU fallback.
|
|
2
|
+
|
|
3
|
+
The model bytes and outputs are unchanged. This module has no runtime imports,
|
|
4
|
+
so provider policy can be tested without downloading models or CUDA libraries.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
LOG = logging.getLogger(__name__)
|
|
11
|
+
CPU = "CPUExecutionProvider"
|
|
12
|
+
CUDA = "CUDAExecutionProvider"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LocalOrtSession:
|
|
16
|
+
"""One ORT session; at most one CPU retry after a GPU failure.
|
|
17
|
+
|
|
18
|
+
Keep the original immutable bytes only while GPU fallback is possible. An
|
|
19
|
+
OTA replacing the file must never change the model used for the retry.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, runtime, model_bytes: bytes, options, accelerator="auto"):
|
|
23
|
+
if accelerator not in ("auto", "cpu"):
|
|
24
|
+
raise ValueError("accelerator must be auto or cpu")
|
|
25
|
+
self._runtime, self._options = runtime, options
|
|
26
|
+
self._model_bytes = model_bytes
|
|
27
|
+
self.requested = accelerator
|
|
28
|
+
self.fallback_reason = None
|
|
29
|
+
self._cuda_active = False
|
|
30
|
+
self._acceleration = {"requested": accelerator, "backend": "starting",
|
|
31
|
+
"providers": [], "fallback_reason": None}
|
|
32
|
+
available = runtime.get_available_providers()
|
|
33
|
+
if accelerator == "auto" and CUDA in available:
|
|
34
|
+
try:
|
|
35
|
+
preload = getattr(runtime, "preload_dlls", None)
|
|
36
|
+
if preload is not None:
|
|
37
|
+
preload(directory="")
|
|
38
|
+
self._session = runtime.InferenceSession(
|
|
39
|
+
model_bytes, sess_options=options, providers=[CUDA, CPU]
|
|
40
|
+
)
|
|
41
|
+
self._cuda_active = CUDA in self._session.get_providers()
|
|
42
|
+
except Exception as error:
|
|
43
|
+
self._cpu("CUDA initialization failed: " + str(error))
|
|
44
|
+
else:
|
|
45
|
+
if not self._cuda_active:
|
|
46
|
+
self._cpu("CUDA initialization returned a CPU-only session")
|
|
47
|
+
else:
|
|
48
|
+
self._session = runtime.InferenceSession(
|
|
49
|
+
model_bytes, sess_options=options, providers=[CPU]
|
|
50
|
+
)
|
|
51
|
+
self._model_bytes = None
|
|
52
|
+
self.fallback_reason = (
|
|
53
|
+
"CPU explicitly selected" if accelerator == "cpu"
|
|
54
|
+
else "CUDA provider unavailable; using the CPU runtime"
|
|
55
|
+
)
|
|
56
|
+
self._publish_acceleration()
|
|
57
|
+
LOG.info("[detector] local execution: %s", self.acceleration)
|
|
58
|
+
|
|
59
|
+
def _cpu(self, reason):
|
|
60
|
+
# A failed GPU attempt must not keep its allocation while CPU starts.
|
|
61
|
+
self._session = None
|
|
62
|
+
self._cuda_active = False
|
|
63
|
+
self.fallback_reason = reason[:1000]
|
|
64
|
+
self._acceleration = {"requested": self.requested, "backend": "initializing_cpu",
|
|
65
|
+
"providers": [], "fallback_reason": self.fallback_reason}
|
|
66
|
+
LOG.warning("[detector] GPU unavailable; CPU fallback: %s", self.fallback_reason)
|
|
67
|
+
self._session = self._runtime.InferenceSession(
|
|
68
|
+
self._model_bytes, sess_options=self._options, providers=[CPU]
|
|
69
|
+
)
|
|
70
|
+
self._model_bytes = None
|
|
71
|
+
self._publish_acceleration()
|
|
72
|
+
|
|
73
|
+
def _publish_acceleration(self):
|
|
74
|
+
providers = list(self._session.get_providers())
|
|
75
|
+
self._acceleration = {
|
|
76
|
+
"requested": self.requested,
|
|
77
|
+
"backend": "cuda" if CUDA in providers else "cpu",
|
|
78
|
+
"providers": providers,
|
|
79
|
+
"fallback_reason": self.fallback_reason,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def acceleration(self):
|
|
84
|
+
# Health must not dereference the transient None session while another
|
|
85
|
+
# thread is constructing CPU fallback. Publish one immutable snapshot.
|
|
86
|
+
return {**self._acceleration, "providers": list(self._acceleration["providers"])}
|
|
87
|
+
|
|
88
|
+
def run(self, outputs, inputs):
|
|
89
|
+
try:
|
|
90
|
+
result = self._session.run(outputs, inputs)
|
|
91
|
+
# ORT may demote internally without raising. Do not keep reporting
|
|
92
|
+
# CUDA just because construction originally requested it.
|
|
93
|
+
if self._cuda_active and CUDA not in self._session.get_providers():
|
|
94
|
+
self._cuda_active = False
|
|
95
|
+
self._model_bytes = None
|
|
96
|
+
self.fallback_reason = "ORT demoted GPU inference to CPU"
|
|
97
|
+
LOG.warning("[detector] %s", self.fallback_reason)
|
|
98
|
+
self._publish_acceleration()
|
|
99
|
+
return result
|
|
100
|
+
except Exception as error:
|
|
101
|
+
if not self._cuda_active:
|
|
102
|
+
raise
|
|
103
|
+
self._cpu("GPU inference failed: " + str(error))
|
|
104
|
+
# Same captured input, once. If CPU also fails, propagate the error;
|
|
105
|
+
# never turn a failed inference into a negative observation.
|
|
106
|
+
return self._session.run(outputs, inputs)
|
|
107
|
+
|
|
108
|
+
def __getattr__(self, name):
|
|
109
|
+
return getattr(self._session, name)
|
|
@@ -128,6 +128,7 @@ class DetectorLoader:
|
|
|
128
128
|
ram_budget_bytes: "int | None" = None,
|
|
129
129
|
vocabularies: "tuple[ModelVocabulary, ...]" = BUILTIN_VOCABULARIES,
|
|
130
130
|
log=print,
|
|
131
|
+
accelerator: str = "auto",
|
|
131
132
|
):
|
|
132
133
|
# `ram_budget_bytes=None` is NOT a default policy — it is the absence of
|
|
133
134
|
# one, and the registry refuses every portfolio decision in that state
|
|
@@ -137,6 +138,7 @@ class DetectorLoader:
|
|
|
137
138
|
self.model_path = model_path
|
|
138
139
|
self.model_name = model_name
|
|
139
140
|
self._intra_op_threads = intra_op_threads
|
|
141
|
+
self._accelerator = accelerator
|
|
140
142
|
self._poll_interval_s = poll_interval_s
|
|
141
143
|
self._log = log
|
|
142
144
|
|
|
@@ -154,7 +156,7 @@ class DetectorLoader:
|
|
|
154
156
|
# ── the two halves that need a real runtime ──────────────────────────────
|
|
155
157
|
|
|
156
158
|
def _build_session(self, entry: ModelEntry) -> OnnxDetector:
|
|
157
|
-
return OnnxDetector(entry.path, entry.vocabulary, self._intra_op_threads)
|
|
159
|
+
return OnnxDetector(entry.path, entry.vocabulary, self._intra_op_threads, self._accelerator)
|
|
158
160
|
|
|
159
161
|
def _probe(self, session: OnnxDetector) -> None:
|
|
160
162
|
"""PROVE IT. A session that constructs is not a session that infers.
|
|
@@ -276,6 +278,7 @@ class DetectorLoader:
|
|
|
276
278
|
"input",
|
|
277
279
|
"classes",
|
|
278
280
|
"providers",
|
|
281
|
+
"acceleration",
|
|
279
282
|
"output_layout",
|
|
280
283
|
"preprocessing",
|
|
281
284
|
):
|
|
@@ -283,8 +286,8 @@ class DetectorLoader:
|
|
|
283
286
|
payload[key] = pinned[key]
|
|
284
287
|
return payload
|
|
285
288
|
|
|
286
|
-
def detect(self, jpeg: bytes, min_confidence: float, wanted: "set[str] | None") -> DetectionBatch:
|
|
287
|
-
return self.registry.detect(jpeg, min_confidence, wanted)
|
|
289
|
+
def detect(self, jpeg: bytes, min_confidence: float, wanted: "set[str] | None", expected_model=None) -> DetectionBatch:
|
|
290
|
+
return self.registry.detect(jpeg, min_confidence, wanted, expected_model=expected_model)
|
|
288
291
|
|
|
289
292
|
# ── the loop ─────────────────────────────────────────────────────────────
|
|
290
293
|
|
|
@@ -62,7 +62,7 @@ import time
|
|
|
62
62
|
from pathlib import Path
|
|
63
63
|
|
|
64
64
|
from .detections import DetectionBatch, merge_batches
|
|
65
|
-
from .errors import DetectorNotReady, ModelContractError, UnknownClasses
|
|
65
|
+
from .errors import DetectorNotReady, ModelContractError, ModelVerificationError, UnknownClasses
|
|
66
66
|
from .labels import ClassIndex, ModelVocabulary, normalize_class_name
|
|
67
67
|
|
|
68
68
|
#: Per-model residency states. `absent` and `idle` are new and they are
|
|
@@ -379,7 +379,7 @@ class ModelRegistry:
|
|
|
379
379
|
|
|
380
380
|
# ── the request path ─────────────────────────────────────────────────────
|
|
381
381
|
|
|
382
|
-
def detect(self, jpeg: bytes, min_confidence: float, wanted: "set[str] | None") -> DetectionBatch:
|
|
382
|
+
def detect(self, jpeg: bytes, min_confidence: float, wanted: "set[str] | None", expected_model=None) -> DetectionBatch:
|
|
383
383
|
"""Run every model this request needs, in catalogue order, and merge.
|
|
384
384
|
|
|
385
385
|
ALL OR NOTHING. If any model in the plan cannot be made resident — the
|
|
@@ -391,7 +391,16 @@ class ModelRegistry:
|
|
|
391
391
|
quieter venue, which is the single failure this component exists to
|
|
392
392
|
prevent.
|
|
393
393
|
"""
|
|
394
|
-
|
|
394
|
+
if expected_model is None:
|
|
395
|
+
plan = self.class_index().assign(wanted)
|
|
396
|
+
else:
|
|
397
|
+
selected = self._states.get(expected_model["modelId"])
|
|
398
|
+
if selected is None:
|
|
399
|
+
raise ModelVerificationError("selected model is not in this box's catalogue")
|
|
400
|
+
vocabulary = selected.entry.vocabulary
|
|
401
|
+
if wanted is not None and not wanted.issubset(vocabulary.class_names):
|
|
402
|
+
raise ModelVerificationError("selected model cannot answer all requested classes")
|
|
403
|
+
plan = [(vocabulary, wanted)]
|
|
395
404
|
if not plan:
|
|
396
405
|
raise DetectorNotReady(
|
|
397
406
|
"no model on this box can answer that request; "
|
|
@@ -400,6 +409,10 @@ class ModelRegistry:
|
|
|
400
409
|
|
|
401
410
|
acquired = self._acquire([vocabulary.model_name for vocabulary, _ in plan])
|
|
402
411
|
try:
|
|
412
|
+
if expected_model is not None and (
|
|
413
|
+
len(acquired) != 1 or getattr(acquired[0], "model_sha256", None) != expected_model["sha256"]
|
|
414
|
+
):
|
|
415
|
+
raise ModelVerificationError("loaded session does not match the selected artifact")
|
|
403
416
|
batches = []
|
|
404
417
|
for (vocabulary, classes), session in zip(plan, acquired):
|
|
405
418
|
state = self._states[vocabulary.model_name]
|
|
@@ -707,6 +720,10 @@ class ModelRegistry:
|
|
|
707
720
|
row["input"] = f"{spec.width}x{spec.height}"
|
|
708
721
|
row["classes"] = spec.num_classes
|
|
709
722
|
row["providers"] = list(spec.providers)
|
|
723
|
+
acceleration = getattr(s.session, "acceleration", None)
|
|
724
|
+
if isinstance(acceleration, dict):
|
|
725
|
+
row["acceleration"] = acceleration
|
|
726
|
+
row["providers"] = acceleration["providers"]
|
|
710
727
|
# WHICH DECODE AND WHICH PREPROCESSING PRODUCED THIS MODEL'S
|
|
711
728
|
# NUMBERS. Three layouts now reach this service and their
|
|
712
729
|
# preprocessors are not interchangeable, so a confidence with
|
|
@@ -805,8 +822,13 @@ def read_process_rss() -> "tuple[int, str] | None":
|
|
|
805
822
|
|
|
806
823
|
counters = _Counters()
|
|
807
824
|
counters.cb = ctypes.sizeof(_Counters)
|
|
808
|
-
|
|
809
|
-
|
|
825
|
+
current_process = ctypes.windll.kernel32.GetCurrentProcess
|
|
826
|
+
current_process.restype = wintypes.HANDLE
|
|
827
|
+
memory_info = ctypes.windll.psapi.GetProcessMemoryInfo
|
|
828
|
+
memory_info.argtypes = [wintypes.HANDLE, ctypes.POINTER(_Counters), wintypes.DWORD]
|
|
829
|
+
memory_info.restype = wintypes.BOOL
|
|
830
|
+
handle = current_process()
|
|
831
|
+
if memory_info(
|
|
810
832
|
handle, ctypes.byref(counters), counters.cb
|
|
811
833
|
):
|
|
812
834
|
return int(counters.WorkingSetSize), "psapi-workingset"
|