Improve performance in packet dissection#5005
Open
polybassa wants to merge 3 commits into
Open
Conversation
…ket processing Profiled packet dissection and optimized the critical path. Benchmark on `Ether/IP/TCP/Raw` (10K iterations): **6153 → 7197 pkt/s (+17%)**, function calls reduced from 6.71M to 4.92M (-27%). ## Changes ### `scapy/fields.py` - **`Field.getfield()`**: Use `struct.unpack_from(buf)` instead of `struct.unpack(buf[:n])` to avoid temporary slice allocation. Falls back to slice for bytes subclasses (e.g. `TrailerBytes`) that override `__getitem__`. - **`Field.m2i/h2i/i2m`**: Remove `typing.cast()` — 260K+ no-op function calls per 10K packets. - **`_FieldContainer`/`Field`**: Add class-level `_is_conditional`/`_may_end` flags to avoid `isinstance()` in tight loops. ### `scapy/packet.py` - **`do_dissect()`**: Check pre-computed field flags instead of `isinstance(ConditionalField)` / `isinstance(MayEnd)` per iteration. - **`guess_payload_class()`**: Inline `getfieldval` with local variable caching of `self.fields`/`self.overloaded_fields`/`self.default_fields`. Original did 3 dict lookups + deprecated field check per field per candidate layer. - **`getfieldval()`**: Replace `if k in d1 ... elif k in d2 ...` with single `try/except KeyError` on fast path. - **`__init__()`**: Skip `time.time()` syscall for internal sub-layer packets (`_internal=1`). - **`_raw_packet_cache_field_value()`**: Replace per-call lambda with direct attribute access. - **`do_init_cached_fields()`**: Eliminate redundant `dict.get()` pattern. ## API No public API changes. ## Dissection Throughput (10K iterations each) | Packet Type | Baseline | Optimized | Δ | |---|---|---|---| | `Ether/IP/TCP/Raw(100B)` | 7,026 pkt/s | 7,508 pkt/s | **+6.9%** | | `Ether/IP/UDP/DNS(query)` | 5,286 pkt/s | 5,685 pkt/s | **+7.5%** | | `Ether/IP/UDP/DNS(response)` | 2,726 pkt/s | 2,889 pkt/s | **+6.0%** | | `Ether/IP/ICMP` | 5,603 pkt/s | 6,000 pkt/s | **+7.1%** | | `IP/TCP/Raw(50B)` | 9,389 pkt/s | 10,063 pkt/s | **+7.2%** | | `Ether/IP/TCP/Raw(1400B)` | 6,870 pkt/s | 7,439 pkt/s | **+8.3%** | | `Ether` (minimal) | 62,548 pkt/s | 64,819 pkt/s | **+3.6%** | | `IP/UDP/Raw(5B)` | 7,998 pkt/s | 8,776 pkt/s | **+9.7%** | | Batch 1000×`Ether/IP/TCP/Raw` (100K pkts) | 7,117 pkt/s | 7,781 pkt/s | **+9.3%** | ## Profile Comparison (5,000 `Ether/IP/TCP/Raw` dissections) | Metric | Baseline | Optimized | Δ | |---|---|---|---| | Total function calls | 2,915,002 | 2,350,002 | **−19.4%** | | Total time | 1.559s | 1.357s | **−13.0%** | | `isinstance()` calls | 380,000 | 230,000 | **−39.5%** | | `guess_payload_class` time | 0.137s | 0.062s | **−55%** | ## Key Observations - Consistent **6–10% throughput improvement** across all packet types - Larger improvement on simpler packets (IP/UDP, IP/TCP) where overhead is proportionally higher - DNS response shows less improvement since most time is spent in complex DNS field parsing - `isinstance()` calls reduced by ~40% via pre-computed `_is_conditional`/`_may_end` flags - `guess_payload_class` is **2× faster** due to inlined field lookups with local variable caching AI-Assisted: yes (Claude Code Opus 4.6)
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #5005 +/- ##
=======================================
Coverage 80.29% 80.29%
=======================================
Files 383 383
Lines 95163 95205 +42
=======================================
+ Hits 76407 76445 +38
- Misses 18756 18760 +4
🚀 New features to boost your workflow:
|
AI-Assisted: no
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes Scapy’s packet dissection hot path by reducing temporary allocations, cutting down on per-field type checks, and inlining/caching common lookups during payload-class guessing.
Changes:
- Optimize
Field.getfield()by usingstruct.unpack_from()for plainbytesto avoid slicing allocations. - Reduce overhead in dissection loops by using precomputed field flags (
_is_conditional,_may_end) instead of repeatedisinstance()checks. - Speed up field/value resolution by replacing membership checks with
try/except KeyError, and by inlininggetfieldval()logic insideguess_payload_class().
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| scapy/packet.py | Reduces per-packet overhead in __init__, getfieldval, do_dissect, and guess_payload_class to improve dissection throughput. |
| scapy/fields.py | Optimizes Field.getfield() and adds class-level flags to enable faster checks in the packet dissection loop. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ): | ||
| # type: (...) -> None | ||
| self.time = time.time() # type: Union[EDecimal, float] | ||
| self.time = 0.0 if _internal else time.time() # type: Union[EDecimal, float] |
AI-Assisted: yes (GitHub Copilot)
699122d to
9b31ee5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I played around with AI tools and it came along with this proposal for performance improvement on Packet dissection.
Profiled packet dissection and optimized the critical path. Benchmark on
Ether/IP/TCP/Raw(10K iterations): 6153 → 7197 pkt/s (+17%), function calls reduced from 6.71M to 4.92M (-27%).Changes
scapy/fields.pyField.getfield(): Usestruct.unpack_from(buf)instead ofstruct.unpack(buf[:n])to avoid temporary slice allocation. Falls back to slice for bytes subclasses (e.g.TrailerBytes) that override__getitem__.Field.m2i/h2i/i2m: Removetyping.cast()— 260K+ no-op function calls per 10K packets._FieldContainer/Field: Add class-level_is_conditional/_may_endflags to avoidisinstance()in tight loops.scapy/packet.pydo_dissect(): Check pre-computed field flags instead ofisinstance(ConditionalField)/isinstance(MayEnd)per iteration.guess_payload_class(): Inlinegetfieldvalwith local variable caching ofself.fields/self.overloaded_fields/self.default_fields. Original did 3 dict lookups + deprecated field check per field per candidate layer.getfieldval(): Replaceif k in d1 ... elif k in d2 ...with singletry/except KeyErroron fast path.__init__(): Skiptime.time()syscall for internal sub-layer packets (_internal=1)._raw_packet_cache_field_value(): Replace per-call lambda with direct attribute access.do_init_cached_fields(): Eliminate redundantdict.get()pattern.API
No public API changes.
Dissection Throughput (10K iterations each)
Ether/IP/TCP/Raw(100B)Ether/IP/UDP/DNS(query)Ether/IP/UDP/DNS(response)Ether/IP/ICMPIP/TCP/Raw(50B)Ether/IP/TCP/Raw(1400B)Ether(minimal)IP/UDP/Raw(5B)Ether/IP/TCP/Raw(100K pkts)Profile Comparison (5,000
Ether/IP/TCP/Rawdissections)isinstance()callsguess_payload_classtimeKey Observations
isinstance()calls reduced by ~40% via pre-computed_is_conditional/_may_endflagsguess_payload_classis 2× faster due to inlined field lookups with local variable cachingAI-Assisted: yes (Claude Code Opus 4.6)