Skip to content

feat(sinsp): CoW individual fdinfos - #3072

Open
gnosek wants to merge 7 commits into
falcosecurity:masterfrom
gnosek:fdinfo-cow
Open

feat(sinsp): CoW individual fdinfos#3072
gnosek wants to merge 7 commits into
falcosecurity:masterfrom
gnosek:fdinfo-cow

Conversation

@gnosek

@gnosek gnosek commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This is the follow up to #3066, where we make individual fds copy on write as well.

What type of PR is this?

Uncomment one (or more) /kind <> lines:

/kind bug

/kind cleanup

/kind design

/kind documentation

/kind failing-test

/kind test

/kind feature

/kind sync

Any specific area of the project related to this PR?

Uncomment one (or more) /area <> lines:

/area API-version

/area build

/area automation

/area drivers

/area driver-kmod

/area driver-modern-bpf

/area libscap-engine-kmod

/area libscap-engine-modern-bpf

/area libscap-engine-nodriver

/area libscap-engine-noop

/area libscap-engine-source-plugin

/area libscap-engine-savefile

/area libscap

/area libpman

/area libsinsp

/area tests

/area proposals

Does this PR require a change in the driver versions?

/version driver-API-version-major

/version driver-API-version-minor

/version driver-API-version-patch

/version driver-SCHEMA-version-major

/version driver-SCHEMA-version-minor

/version driver-SCHEMA-version-patch

What this PR does / why we need it:

Which issue(s) this PR fixes:

Fixes #

Special notes for your reviewer:

Does this PR introduce a user-facing change?:

NONE

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Perf diff from master - unit tests

    18.90%     -7.58%  [.] std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_add_ref_lock_nothrow()
    10.73%     +5.84%  [.] sinsp_thread_manager::create_thread_dependencies(std::shared_ptr<sinsp_threadinfo> const&)
    19.83%     -5.04%  [.] sinsp_threadinfo::get_main_thread()
    14.27%     +5.03%  [.] std::__shared_ptr<sinsp_threadinfo, (__gnu_cxx::_Lock_policy)2>::__shared_ptr(std::__weak_ptr<sinsp_threadinfo, (__gnu_cxx::_Lock_policy)2> const&, std::nothrow_t)
     4.59%     +2.89%  [.] std::__shared_count<(__gnu_cxx::_Lock_policy)2>::_M_get_use_count() const
     7.24%     -1.48%  [.] std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count(std::__weak_count<(__gnu_cxx::_Lock_policy)2> const&, std::nothrow_t)
     3.63%     +0.26%  [.] sinsp_threadinfo::get_fd_table() const
     0.50%     -0.17%  [.] scap_event_encode_params_v
     0.09%     +0.12%  [.] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose()@plt
     0.26%     -0.09%  [.] sinsp_utils::find_longest_matching_evt_param(std::basic_string_view<char, std::char_traits<char> >)

Heap diff from master - unit tests

peak heap memory consumption: -85.92K
peak RSS (including heaptrack overhead): 0B
total memory leaked: 0B

Heap diff from master - scap file

peak heap memory consumption: 33.63K
peak RSS (including heaptrack overhead): 0B
total memory leaked: 0B

Benchmarks diff from master

Comparing gbench_data.json to /root/actions-runner/_work/libs/libs/build/gbench_data.json
Benchmark                                                                             Time             CPU      Time Old      Time New       CPU Old       CPU New
------------------------------------------------------------------------------------------------------------------------------------------------------------------
BM_sinsp_split_mean                                                                -0.0047         -0.0047           288           287           288           286
BM_sinsp_split_median                                                              -0.0008         -0.0006           287           287           287           287
BM_sinsp_split_stddev                                                              -0.2699         -0.2667             5             4             5             4
BM_sinsp_split_cv                                                                  -0.2664         -0.2632             0             0             0             0
BM_sinsp_concatenate_paths_relative_path_mean                                      +0.0531         +0.0533            67            71            67            71
BM_sinsp_concatenate_paths_relative_path_median                                    +0.0477         +0.0479            67            71            67            71
BM_sinsp_concatenate_paths_relative_path_stddev                                    +4.7611         +4.7911             0             1             0             1
BM_sinsp_concatenate_paths_relative_path_cv                                        +4.4707         +4.4980             0             0             0             0
BM_sinsp_concatenate_paths_empty_path_mean                                         +0.0707         +0.0709            45            48            45            48
BM_sinsp_concatenate_paths_empty_path_median                                       +0.0815         +0.0816            44            48            44            48
BM_sinsp_concatenate_paths_empty_path_stddev                                       -0.9256         -0.9238             1             0             1             0
BM_sinsp_concatenate_paths_empty_path_cv                                           -0.9305         -0.9289             0             0             0             0
BM_sinsp_concatenate_paths_absolute_path_mean                                      +0.0374         +0.0374            66            69            66            69
BM_sinsp_concatenate_paths_absolute_path_median                                    +0.0409         +0.0410            66            69            66            69
BM_sinsp_concatenate_paths_absolute_path_stddev                                    +3.5786         +3.5502             0             1             0             1
BM_sinsp_concatenate_paths_absolute_path_cv                                        +3.4135         +3.3862             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_ascii_short_mean                                  -0.0007         -0.0007            15            15            15            15
BM_sinsp_utf8_sanitize_fast_path_ascii_short_median                                -0.0008         -0.0007            15            15            15            15
BM_sinsp_utf8_sanitize_fast_path_ascii_short_stddev                                +0.3677         -0.0905             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_ascii_short_cv                                    +0.3686         -0.0898             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_ascii_long_mean                                   +0.0009         +0.0008            87            87            87            87
BM_sinsp_utf8_sanitize_fast_path_ascii_long_median                                 +0.0004         +0.0004            87            87            87            87
BM_sinsp_utf8_sanitize_fast_path_ascii_long_stddev                                 +1.8896         +1.5664             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_ascii_long_cv                                     +1.8870         +1.5644             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_multibyte_short_mean                              +0.0044         +0.0044            12            12            12            12
BM_sinsp_utf8_sanitize_fast_path_multibyte_short_median                            +0.0000         +0.0000            12            12            12            12
BM_sinsp_utf8_sanitize_fast_path_multibyte_short_stddev                           +73.6190        +72.4552             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_multibyte_short_cv                               +73.2941        +72.1341             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_multibyte_long_mean                               +0.0805         +0.0805          3801          4108          3800          4106
BM_sinsp_utf8_sanitize_fast_path_multibyte_long_median                             +0.0802         +0.0803          3801          4106          3800          4105
BM_sinsp_utf8_sanitize_fast_path_multibyte_long_stddev                             +3.6990         +2.9603             1             4             1             4
BM_sinsp_utf8_sanitize_fast_path_multibyte_long_cv                                 +3.3488         +2.6651             0             0             0             0
BM_sinsp_utf8_sanitize_fast_path_mixed_long_mean                                   +0.0544         +0.0545          1676          1767          1675          1767
BM_sinsp_utf8_sanitize_fast_path_mixed_long_median                                 +0.0513         +0.0512          1683          1769          1683          1769
BM_sinsp_utf8_sanitize_fast_path_mixed_long_stddev                                 -0.8151         -0.8167            25             5            25             5
BM_sinsp_utf8_sanitize_fast_path_mixed_long_cv                                     -0.8247         -0.8261             0             0             0             0
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_alloc_mean                       +0.0552         +0.0552          1115          1176          1114          1176
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_alloc_median                     +0.0669         +0.0668          1114          1189          1114          1188
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_alloc_stddev                    +15.5003        +16.0375             2            27             2            27
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_alloc_cv                        +14.6371        +15.1468             0             0             0             0
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_noalloc_mean                     +0.0083         +0.0084          1112          1121          1111          1121
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_noalloc_median                   +0.0059         +0.0059          1111          1118          1111          1118
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_noalloc_stddev                  +11.4328        +11.0998             1            12             1            12
BM_sinsp_utf8_sanitize_slow_path_c1_controls_long_noalloc_cv                      +11.3299        +10.9995             0             0             0             0
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_alloc_mean                    -0.0114         -0.0114           235           233           235           233
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_alloc_median                  -0.0108         -0.0108           234           231           234           231
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_alloc_stddev                  +0.1441         +0.1423             4             4             4             4
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_alloc_cv                      +0.1573         +0.1556             0             0             0             0
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_noalloc_mean                  +0.0080         +0.0082           143           145           143           144
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_noalloc_median                +0.0085         +0.0086           142           143           142           143
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_noalloc_stddev                -0.0257         -0.0185             2             2             2             2
BM_sinsp_utf8_sanitize_slow_path_sparse_invalid_long_noalloc_cv                    -0.0334         -0.0265             0             0             0             0
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_alloc_mean                       -0.0854         -0.0855         11817         10808         11812         10803
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_alloc_median                     -0.1055         -0.1058         11749         10509         11744         10502
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_alloc_stddev                     +1.9131         +1.9272           187           545           186           546
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_alloc_cv                         +2.1850         +2.2008             0             0             0             0
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_noalloc_mean                     +0.0245         +0.0246         11311         11589         11307         11585
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_noalloc_median                   +0.0388         +0.0388         11178         11611         11174         11607
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_noalloc_stddev                   -0.9320         -0.9312          1056            72          1056            73
BM_sinsp_utf8_sanitize_slow_path_all_invalid_long_noalloc_cv                       -0.9336         -0.9329             0             0             0             0

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.00699% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.69%. Comparing base (490140d) to head (a498d54).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
userspace/libsinsp/sinsp.cpp 11.11% 8 Missing ⚠️
userspace/libsinsp/fdtable.cpp 87.23% 6 Missing ⚠️
...erspace/libsinsp/test/fdinfo_content_equals.ut.cpp 95.62% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3072      +/-   ##
==========================================
+ Coverage   76.43%   76.69%   +0.26%     
==========================================
  Files         301      302       +1     
  Lines       33659    33968     +309     
  Branches     5054     5058       +4     
==========================================
+ Hits        25726    26051     +325     
+ Misses       7933     7917      -16     
Flag Coverage Δ
libsinsp 76.69% <93.00%> (+0.26%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leogr

leogr commented Jul 30, 2026

Copy link
Copy Markdown
Member

/milestone 0.26.0

@poiana poiana added this to the 0.26.0 milestone Jul 30, 2026

@leogr leogr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed using https://github.com/leogr/falco-expert

One thing I believe we may want to address here: content_equals() is not virtual, while sinsp_fdinfo is an explicit subclassing point via build_fdinfo(). See my comments below 👇

Also, since this PR changes the plugin-visible behavior of get_table_entry() handles, I am not fully convinced that release-note: NONE is the right call.

Everything else is minor. Thanks 🙏

Comment thread userspace/libsinsp/fdinfo.h Outdated
Comment thread userspace/libsinsp/fdinfo.cpp
Comment thread userspace/libsinsp/fdtable.cpp
Comment thread userspace/libsinsp/fdtable.cpp Outdated
Comment thread userspace/libsinsp/sinsp.cpp Outdated
Comment thread userspace/libsinsp/fdtable.cpp
gnosek added 7 commits July 31, 2026 13:45
Two comments that were saying less than they should:

upgrade_fd_info_writable() claimed that when the writable lookup comes up
empty "callers cope with that the same way they cope with a null fd info".
They don't -- they never see null, they get the pre-existing read-only
pointer and dereference it. Spell out what a write through it would mean
and what has to happen first for that to be reachable.

The m_fdinfo_name_snapshot comment still carried the first line of the
bool field it replaced, and did not say that snapshot validity is keyed on
the fd pointer: replacing the event's fd mid-event also clears the
name-change answer.

Comments only, no functional change.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
Writable access paths (find_mut, loop, the plugin API's get_entry)
now give the slot a private entry before handing out mutable access:
if the entry may be referenced by another table, it is cloned in place.

The copy decision is gated on a per-table hint, m_entries_maybe_shared,
set when share_from() puts contents in play and cleared when a private
generation is guaranteed (deep detach, clear, construction). The gate is
what makes plain reference counts usable: on a never-shared table, extra
references are plugin entry handles or caches, which must keep observing
in-place writes -- copying there would orphan a handle someone is about
to write through (the thread_manager.fdtable_access add_entry/get_entry
pattern does exactly this). On a once-shared table the count check runs,
disarming our own cache reference first; a stale cache elsewhere then
costs nothing but the copy, while a live plugin handle is left pointing
at the pre-copy entry, which is why get_entry() now spells out how long
a handle is good for.

find_mut also gains a cache fast path for the private-entry case,
mirroring the const lookup cost on the hot per-event path.

Inert for now: the detach is still deep, so cross-table entry references
do not survive it. The shallow flip comes next.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
detach_if_shared() now copies only the map, sharing the entries with the
previous contents; retain() on shared contents likewise keeps the
surviving slots without cloning them. Individual entries are copied at
writable handout (the machinery from the previous commit), so the cost
of touching a table drops from "clone every fd" to "copy the map nodes"
and each modified fd pays exactly one entry clone per table.

Inherited fds that no process ever modifies now exist once, for their
whole life, no matter how many processes hold them or how much unrelated
fd activity those processes have -- this is where the memory win on
fork-heavy workloads (hundreds of thousands of duplicated fdinfos)
reaches its full size. The fork+execve pattern shares even the
surviving non-CLOEXEC entries.

Tests extended to pin the granularity: a write copies only the touched
entry, add/retain/execve-purge leave untouched entries shared across
tables.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
The debug dump at the end of a capture said how many fds the thread table
holds in total, which after copy-on-write sharing is a count of fd slots,
not of fd entries: an fd inherited by a thousand processes is one entry
billed a thousand times. Report both, so the gap is the saving.

Unique means distinct entry objects, not distinct contents maps. Sharing
starts wholesale, but the first write anywhere in a table gives it a
private map that still references the parent's entries, and per-entry
sharing is the thing this series is about -- counting maps would stop
seeing it exactly when it starts working.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
Full-state equality for fd entries: fd number, type, flags, identity
(dev/mount/ino), names, and the union member selected by the type (the
tuple unions compare their raw bytes, the server infos field by field to
sidestep padding). Dynamic fields are not compared, so
extensible_struct exposes has_dynamic_field_values() letting callers
exclude entries that carry any.

Virtual, like clone() and for the same reason: an event processor may
build a subclass carrying extra data through build_fdinfo(), and two such
entries agreeing on every base field are still not interchangeable. A
predicate that could not see the extra data would answer true and let a
caller share one entry where two are needed, with no way back once the
first write clones the survivor.

This is the equality the proc-scan deduplication pass (next commit)
uses to decide that two scanned fds are the same inherited fd. Nothing
about a wrong answer is loud -- it merges two distinct fds and lets one
process observe the other's state -- so the tests walk every field of
every fd type, change it alone, and demand that the answer flips.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
lookup_device() cleared m_mount_id whichever way the lookup went, so a
failure was recorded as "resolved, to nothing": the entry kept m_dev == 0
and could never be asked again. The lookup fails whenever
/proc/<tid>/mountinfo cannot be read, most obviously when the scanned
process is already gone by the time one of its fds is first looked up.

Clear the mount id only when it produced a device. Since libscap caches
successful lookups globally in m_dev_list, keyed by mount id and
independent of the tid, this costs a retry only for the entries that have
no answer yet -- and a retry is exactly what they want: an fd's mount is
listed in the mountinfo of every process that holds a file on it, so a
table whose own tid is still readable can succeed where another one
failed.

Own commit because it is not new (9dc846f introduced the "don't try
again" comment along with the behaviour) and it stands on its own. It is
here because sharing changes what it costs: with entries shared between
fd tables, one table giving up now takes the device away from every other
table holding the same entry, where before it only ever cost the one
process that looked first.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
…scan

Fork-inherited fds appear once per process in /proc, so the initial
scan materializes the same duplication the live fork path no longer
creates: tens of thousands of processes can carry hundreds of thousands
of copies of a few thousand distinct fds. After the scan (and after the
socket direction fixups, which mutate entries), a single pass shares
content-identical entries between tables, bucketed by the
fork-inheritance signature (fd number, device, inode) and confirmed by
full content equality.

Adopting or donating a canonical entry flips the table's
entries-maybe-shared hint, so copy-on-write isolates any later
divergence exactly as it does for live forks; entries carrying dynamic
field values are excluded (content_equals does not cover them), as are
tables sharing contents wholesale (already deduplicated, and their maps
must not be rebound in place).

The pass runs in sinsp init for both live captures and scap files.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
@poiana poiana added size/XXL and removed size/XL labels Jul 31, 2026
@gnosek
gnosek requested a review from leogr July 31, 2026 12:26
@poiana

poiana commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

LGTM label has been added.

DetailsGit tree hash: e501585b1a87792ea0e4a34cbda921af6b207f01

@poiana

poiana commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: gnosek, leogr

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants