Skip to content

Commit d41eb50

Browse files
committed
Bugfix: release multi analysis payload memory
- drop retained raw transaction bytes after analysis responses - clear shared multi-analysis intermediates after response build - apply post-analysis cleanup across all multi analysis endpoints - preserve metadata while releasing capture payload memory - add pytest coverage for transaction payload cleanup - 2026-03-19 01:51:18
1 parent abcf82e commit d41eb50

9 files changed

Lines changed: 103 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66

77
[project]
88
name = "pypnm-docsis"
9-
version = "1.5.6.0"
9+
version = "1.5.6.1"
1010
description = "DOCSIS 3.x/4.0 Proactive Network Maintenance Toolkit"
1111
readme = "README.md"
1212
requires-python = ">=3.10"

src/pypnm/api/routes/advance/analysis/report/multi_analysis_rpt.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from pypnm.lib.db.json_transaction import JsonTransactionDb
2525
from pypnm.lib.mac_address import MacAddress, cast
2626
from pypnm.lib.matplot.manager import MatplotManager
27+
from pypnm.lib.memory import ProcessMemory
2728
from pypnm.lib.types import ChannelId, JSONScalar, PathArray, PathLike, TimeStamp
2829
from pypnm.lib.utils import Generate, TimeUnit
2930

@@ -258,6 +259,19 @@ def getTransactionCollection(self) -> TransactionCollection:
258259
"""Return the `TransactionCollection` instance used to collect capture files."""
259260
return self._trans_collect
260261

262+
def release_analysis_memory(self) -> None:
263+
"""
264+
Release heavy analysis intermediates once the final response/artifact exists.
265+
266+
This drops raw transaction payloads and common per-channel analysis models
267+
that are no longer needed after the caller has built its response body or
268+
archive path.
269+
"""
270+
self._common_analysis_model = {}
271+
self._capt_data_agg.release_payload_bytes()
272+
self._trans_collect.release_payload_bytes()
273+
ProcessMemory.release_unused_memory()
274+
261275
def _pluck_system_description_model(self) -> SystemDescriptorModel | None:
262276
"""
263277
Scan collected transactions and return the first non-empty sysDescr model.

src/pypnm/api/routes/advance/common/capture_data_aggregator.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515
from pypnm.api.routes.common.classes.file_capture.types import TransactionRecordModel
1616
from pypnm.config.system_config_settings import SystemConfigSettings
17+
from pypnm.lib.memory import ProcessMemory
1718
from pypnm.lib.types import FileNameStr, GroupId, TransactionId
1819
from pypnm.pnm.lib.pnm_artifact_store import PnmArtifactStore
1920

@@ -87,6 +88,17 @@ def collect(self) -> TransactionCollection:
8788

8889
return self._trans_collection
8990

91+
def release_payload_bytes(self) -> None:
92+
"""
93+
Release retained raw capture bytes after analysis output is prepared.
94+
95+
This preserves transaction metadata while dropping the byte payloads held
96+
by the underlying transaction collection.
97+
"""
98+
self._trans_collection.release_payload_bytes()
99+
self._trans_file_bin_entries = []
100+
ProcessMemory.release_unused_memory()
101+
90102
# ──────────────────────────────────────────────────────────────────────
91103
# Helpers
92104
# ──────────────────────────────────────────────────────────────────────

src/pypnm/api/routes/advance/common/transactionsCollection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pypnm.api.routes.advance.common.types.types import Sort
1313
from pypnm.api.routes.common.classes.file_capture.types import TransactionRecordModel
1414
from pypnm.lib.mac_address import MacAddress
15+
from pypnm.lib.memory import ProcessMemory
1516
from pypnm.lib.types import ByteArray, TransactionId
1617

1718

@@ -177,6 +178,23 @@ def getTransactionBytes(self, transaction_id: TransactionId = "",
177178
def getMacAddresses(self) -> list[MacAddress]:
178179
return list(self._mac_addresses)
179180

181+
def release_payload_bytes(self) -> None:
182+
"""
183+
Drop retained raw capture bytes while preserving transaction metadata.
184+
185+
This is intended for post-analysis cleanup where the parsed result model
186+
has already been built and the original file payloads are no longer
187+
required in memory.
188+
"""
189+
for record in self._records:
190+
record.data = b""
191+
for record in self._transaction_models:
192+
record.data = b""
193+
for record in self._transaction_tm.values():
194+
record.data = b""
195+
196+
ProcessMemory.release_unused_memory()
197+
180198
def _sorted_records(self, sorts: list[Sort], reverse: bool) -> list[TransactionCollectionModel]:
181199
"""
182200
Internal helper to sort records based on provided sort keys.

src/pypnm/api/routes/advance/multi_ds_chan_est/router.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,18 +282,21 @@ def analysis(request: MultiChanEstAnalysisRequest) -> MultiChanEstimationAnalysi
282282
"system_description": analysis_result.system_description,
283283
}
284284

285-
self._release_operation_memory(request.operation_id)
286-
return MultiChanEstimationAnalysisResponse(
285+
response = MultiChanEstimationAnalysisResponse(
287286
mac_address = mac,
288287
status = status,
289288
message = message,
290289
**response_kwargs,
291290
data = data_model)
291+
engine.release_analysis_memory()
292+
self._release_operation_memory(request.operation_id)
293+
return response
292294

293295
elif output_type == OutputType.ARCHIVE:
294296
try:
295297
rpt = engine.build_report()
296298
self.logger.info(f"[analysis] Built archive report for group {capture_group_id}")
299+
engine.release_analysis_memory()
297300
self._release_operation_memory(request.operation_id)
298301
return PnmFileService().get_file(FileType.ARCHIVE, rpt.name)
299302

src/pypnm/api/routes/advance/multi_rxmer/router.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,16 +513,19 @@ def analysis(request: MultiRxMerAnalysisRequest) -> MultiRxMerAnalysisResponse |
513513
"mac_address": mac_address,
514514
"system_description": response_system_description,
515515
}
516-
self._release_operation_memory(request.operation_id)
517-
return MultiRxMerAnalysisResponse(
516+
response = MultiRxMerAnalysisResponse(
518517
mac_address = mac_address,
519518
status = status_code,
520519
message = response_message,
521520
**response_kwargs,
522521
data = data,)
522+
engine.release_analysis_memory()
523+
self._release_operation_memory(request.operation_id)
524+
return response
523525

524526
elif output_type == OutputType.ARCHIVE:
525527
rpt = engine.build_report()
528+
engine.release_analysis_memory()
526529
self._release_operation_memory(request.operation_id)
527530
return PnmFileService().get_file(FileType.ARCHIVE, rpt.name)
528531

src/pypnm/api/routes/advance/multi_us_ofdma_pre_eq/router.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,18 +280,21 @@ def analysis(request: MultiUsOfdmaPreEqAnalysisRequest) -> MultiUsOfdmaPreEqAnal
280280
"system_description": analysis_result.system_description,
281281
}
282282

283-
self._release_operation_memory(request.operation_id)
284-
return MultiUsOfdmaPreEqAnalysisResponse(
283+
response = MultiUsOfdmaPreEqAnalysisResponse(
285284
mac_address = mac,
286285
status = status,
287286
message = message,
288287
**response_kwargs,
289288
data = data_model)
289+
engine.release_analysis_memory()
290+
self._release_operation_memory(request.operation_id)
291+
return response
290292

291293
elif output_type == OutputType.ARCHIVE:
292294
try:
293295
rpt = engine.build_report()
294296
self.logger.info(f"[analysis] Built archive report for group {capture_group_id}")
297+
engine.release_analysis_memory()
295298
self._release_operation_memory(request.operation_id)
296299
return PnmFileService().get_file(FileType.ARCHIVE, rpt.name)
297300

src/pypnm/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
__all__ = ["__version__"]
77

88
# MAJOR.MINOR.MAINTENANCE.BUILD
9-
__version__: str = "1.5.6.0"
9+
__version__: str = "1.5.6.1"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (c) 2026
3+
4+
from __future__ import annotations
5+
6+
from pypnm.api.routes.advance.common.transactionsCollection import TransactionCollection
7+
from pypnm.api.routes.common.classes.file_capture.types import (
8+
CompressionMetadataModel,
9+
DeviceDetailsModel,
10+
TransactionRecordModel,
11+
)
12+
from pypnm.docsis.cm_snmp_operation import SystemDescriptor
13+
14+
15+
def test_transaction_collection_release_payload_bytes() -> None:
16+
collection = TransactionCollection()
17+
record = TransactionRecordModel(
18+
transaction_id="tx-1",
19+
timestamp=1,
20+
mac_address="aa:bb:cc:dd:ee:ff",
21+
pnm_test_type="DS_OFDM_RXMER",
22+
filename="sample.bin",
23+
compression=CompressionMetadataModel(
24+
is_compressed=False,
25+
codec="none",
26+
level=0,
27+
size_before=7,
28+
size_after=7,
29+
),
30+
device_details=DeviceDetailsModel(system_description=SystemDescriptor.empty().to_model()),
31+
)
32+
33+
collection.add(record, b"payload")
34+
35+
assert collection.getTransactionBytes() == [b"payload"]
36+
assert collection.getTransactionCollectionModel()[0].data == b"payload"
37+
38+
collection.release_payload_bytes()
39+
40+
assert collection.getTransactionBytes() == [b""]
41+
assert collection.getTransactionCollectionModel()[0].data == b""
42+
assert collection.getTransactionIds() == ["tx-1"]

0 commit comments

Comments
 (0)