@@ -591,7 +591,7 @@ static cbm_proc_tree_rss_status_t cbm_subprocess_tree_rss_platform(cbm_subproces
591591 * all-process scan cannot decide membership: proc_pidinfo answers EPERM for
592592 * every process of another user, so "not in our group" and "in our group but
593593 * unreadable" would look the same. Returns the member count, or -1. */
594- static int cbm_tree_rss_list_group ( pid_t pgid , pid_t * * pids_out ) {
594+ static int cbm_tree_rss_list_pids ( uint32_t type , uint32_t arg , pid_t * * pids_out ) {
595595 int capacity = 64 ;
596596 pid_t * pids = NULL ;
597597 for (;;) {
@@ -602,8 +602,7 @@ static int cbm_tree_rss_list_group(pid_t pgid, pid_t **pids_out) {
602602 }
603603 pids = grown ;
604604 errno = 0 ;
605- int bytes =
606- proc_listpids (PROC_PGRP_ONLY , (uint32_t )pgid , pids , capacity * (int )sizeof (* pids ));
605+ int bytes = proc_listpids (type , arg , pids , capacity * (int )sizeof (* pids ));
607606 if (bytes < 0 || (bytes == 0 && errno != 0 )) {
608607 cbm_free (CBM_MEM_CLASS_OTHER , pids );
609608 return -1 ;
@@ -621,6 +620,63 @@ static int cbm_tree_rss_list_group(pid_t pgid, pid_t **pids_out) {
621620 }
622621}
623622
623+ /* Bound on the processes one probe retains; past it the probe fails closed. */
624+ enum { CBM_APPLE_PROBE_MAX_PROCS = 65536 };
625+
626+ /* The worker's process group plus every descendant by parent pid. Children
627+ * placed in a group of their own (git discovery) are not in the group list.
628+ * Pids already collected are skipped, so nothing is counted twice. Returns the
629+ * count, or -1. */
630+ static int cbm_tree_rss_list_tree (pid_t pgid , pid_t * * pids_out ) {
631+ pid_t * tree = NULL ;
632+ int count = cbm_tree_rss_list_pids (PROC_PGRP_ONLY , (uint32_t )pgid , & tree );
633+ if (count < 0 ) {
634+ return -1 ;
635+ }
636+ int capacity = count > 0 ? count : 1 ;
637+ for (int index = 0 ; index < count ; index ++ ) {
638+ pid_t * children = NULL ;
639+ int child_count = tree [index ] > 0 ? cbm_tree_rss_list_pids (PROC_PPID_ONLY ,
640+ (uint32_t )tree [index ], & children )
641+ : 0 ;
642+ if (child_count < 0 ) {
643+ /* ESRCH-style races surface as an empty listing; a real error
644+ * must not under-report the tree. */
645+ cbm_free (CBM_MEM_CLASS_OTHER , tree );
646+ return -1 ;
647+ }
648+ for (int child = 0 ; child < child_count ; child ++ ) {
649+ bool known = children [child ] <= 0 ;
650+ for (int seen = 0 ; !known && seen < count ; seen ++ ) {
651+ known = tree [seen ] == children [child ];
652+ }
653+ if (known ) {
654+ continue ;
655+ }
656+ if (count >= CBM_APPLE_PROBE_MAX_PROCS ) {
657+ cbm_free (CBM_MEM_CLASS_OTHER , children );
658+ cbm_free (CBM_MEM_CLASS_OTHER , tree );
659+ return -1 ;
660+ }
661+ if (count == capacity ) {
662+ pid_t * grown =
663+ cbm_realloc (CBM_MEM_CLASS_OTHER , tree , (size_t )capacity * 2 * sizeof (* tree ));
664+ if (!grown ) {
665+ cbm_free (CBM_MEM_CLASS_OTHER , children );
666+ cbm_free (CBM_MEM_CLASS_OTHER , tree );
667+ return -1 ;
668+ }
669+ tree = grown ;
670+ capacity *= 2 ;
671+ }
672+ tree [count ++ ] = children [child ];
673+ }
674+ cbm_free (CBM_MEM_CLASS_OTHER , children );
675+ }
676+ * pids_out = tree ;
677+ return count ;
678+ }
679+
624680static int cbm_tree_rss_member_rusage (pid_t pid , bool root , struct rusage_info_v2 * usage ) {
625681#ifdef CBM_ENABLE_TEST_SEAMS
626682 int fault = cbm_tree_rss_member_fault (root );
@@ -637,7 +693,7 @@ static int cbm_tree_rss_member_rusage(pid_t pid, bool root, struct rusage_info_v
637693static cbm_proc_tree_rss_status_t cbm_subprocess_tree_rss_platform (cbm_subprocess_t * process ,
638694 uint64_t * rss_bytes ) {
639695 pid_t * pids = NULL ;
640- int count = cbm_tree_rss_list_group (process -> pgid , & pids );
696+ int count = cbm_tree_rss_list_tree (process -> pgid , & pids );
641697 if (count < 0 ) {
642698 return CBM_PROC_TREE_RSS_ERROR ;
643699 }
@@ -730,8 +786,8 @@ static cbm_linux_stat_t cbm_linux_proc_stat_line(const char *pid_name, bool root
730786 return CBM_LINUX_STAT_OK ;
731787}
732788
733- static cbm_linux_stat_t cbm_linux_proc_stat (const char * pid_name , bool root , pid_t * group ,
734- int64_t * rss_pages ) {
789+ static cbm_linux_stat_t cbm_linux_proc_stat (const char * pid_name , bool root , pid_t * parent ,
790+ pid_t * group , int64_t * rss_pages ) {
735791 char stat_line [4096 ];
736792 cbm_linux_stat_t status =
737793 cbm_linux_proc_stat_line (pid_name , root , stat_line , sizeof (stat_line ));
@@ -746,16 +802,20 @@ static cbm_linux_stat_t cbm_linux_proc_stat(const char *pid_name, bool root, pid
746802 char * save = NULL ;
747803 char * token = strtok_r (command_end + 2 , " " , & save );
748804 int field = 3 ;
805+ bool have_parent = false;
749806 bool have_group = false;
750807 bool have_rss = false;
751808 while (token ) {
752- if (field == 5 || field == 24 ) {
809+ if (field == 4 || field == 5 || field == 24 ) {
753810 char * end = NULL ;
754811 long long value = strtoll (token , & end , 10 );
755812 if (!end || * end != '\0' ) {
756813 return CBM_LINUX_STAT_UNREADABLE ;
757814 }
758- if (field == 5 ) {
815+ if (field == 4 ) {
816+ * parent = (pid_t )value ;
817+ have_parent = true;
818+ } else if (field == 5 ) {
759819 * group = (pid_t )value ;
760820 have_group = true;
761821 } else {
@@ -767,7 +827,76 @@ static cbm_linux_stat_t cbm_linux_proc_stat(const char *pid_name, bool root, pid
767827 token = strtok_r (NULL , " " , & save );
768828 field ++ ;
769829 }
770- return have_group && have_rss ? CBM_LINUX_STAT_OK : CBM_LINUX_STAT_UNREADABLE ;
830+ return have_parent && have_group && have_rss ? CBM_LINUX_STAT_OK : CBM_LINUX_STAT_UNREADABLE ;
831+ }
832+
833+ /* One scanned process. `counted` is set once it is known to belong to the
834+ * worker tree. */
835+ typedef struct {
836+ pid_t pid ;
837+ pid_t parent ;
838+ pid_t group ;
839+ uint64_t bytes ;
840+ bool counted ;
841+ } cbm_linux_proc_t ;
842+
843+ /* Bound on the processes one probe retains. Past it the probe fails closed
844+ * instead of reporting a total that may miss part of the tree. */
845+ enum { CBM_LINUX_PROBE_MAX_PROCS = 262144 , CBM_LINUX_PROBE_INITIAL_PROCS = 512 };
846+
847+ static bool cbm_linux_probe_push (cbm_linux_proc_t * * procs , size_t * count , size_t * capacity ,
848+ const cbm_linux_proc_t * proc ) {
849+ if (* count == * capacity ) {
850+ if (* capacity >= CBM_LINUX_PROBE_MAX_PROCS ) {
851+ return false;
852+ }
853+ size_t grown_capacity = * capacity ? * capacity * 2 : CBM_LINUX_PROBE_INITIAL_PROCS ;
854+ cbm_linux_proc_t * grown =
855+ cbm_realloc (CBM_MEM_CLASS_OTHER , * procs , grown_capacity * sizeof (* * procs ));
856+ if (!grown ) {
857+ return false;
858+ }
859+ * procs = grown ;
860+ * capacity = grown_capacity ;
861+ }
862+ (* procs )[(* count )++ ] = * proc ;
863+ return true;
864+ }
865+
866+ static int cbm_linux_probe_by_pid (const void * left , const void * right ) {
867+ pid_t a = ((const cbm_linux_proc_t * )left )-> pid ;
868+ pid_t b = ((const cbm_linux_proc_t * )right )-> pid ;
869+ return (a > b ) - (a < b );
870+ }
871+
872+ /* Mark every process of the worker tree: members of its process group, plus
873+ * any descendant by parent pid. Descendants can sit in a group of their own
874+ * (git discovery children are spawned with setpgid), so the group alone would
875+ * miss them. `procs` is sorted by pid, so a parent is found by binary search
876+ * and each pass is O(n log n); passes repeat until no process is added (at
877+ * most the tree depth). A process is counted once because the flag is checked
878+ * before it is set. */
879+ static void cbm_linux_probe_mark_tree (cbm_linux_proc_t * procs , size_t count , pid_t pgid ) {
880+ qsort (procs , count , sizeof (* procs ), cbm_linux_probe_by_pid );
881+ for (size_t index = 0 ; index < count ; index ++ ) {
882+ procs [index ].counted = procs [index ].group == pgid ;
883+ }
884+ bool changed = true;
885+ while (changed ) {
886+ changed = false;
887+ for (size_t index = 0 ; index < count ; index ++ ) {
888+ if (procs [index ].counted || procs [index ].parent <= 1 ) {
889+ continue ;
890+ }
891+ cbm_linux_proc_t key = {.pid = procs [index ].parent };
892+ const cbm_linux_proc_t * parent =
893+ bsearch (& key , procs , count , sizeof (* procs ), cbm_linux_probe_by_pid );
894+ if (parent && parent -> counted ) {
895+ procs [index ].counted = true;
896+ changed = true;
897+ }
898+ }
899+ }
771900}
772901
773902static cbm_proc_tree_rss_status_t cbm_subprocess_tree_rss_platform (cbm_subprocess_t * process ,
@@ -782,36 +911,54 @@ static cbm_proc_tree_rss_status_t cbm_subprocess_tree_rss_platform(cbm_subproces
782911 return CBM_PROC_TREE_RSS_ERROR ;
783912 }
784913
785- uint64_t total = 0 ;
786- int measured = 0 ;
914+ cbm_linux_proc_t * procs = NULL ;
915+ size_t count = 0 ;
916+ size_t capacity = 0 ;
787917 bool failed = false;
788918 struct dirent * entry ;
789919 while (!failed && (entry = readdir (proc )) != NULL ) {
790920 if (!cbm_proc_pid_name (entry -> d_name )) {
791921 continue ;
792922 }
923+ pid_t parent = 0 ;
793924 pid_t group = 0 ;
794925 int64_t pages = 0 ;
795- bool root_entry = strtol (entry -> d_name , NULL , 10 ) == (long )process -> pid ;
796- cbm_linux_stat_t status = cbm_linux_proc_stat (entry -> d_name , root_entry , & group , & pages );
926+ long pid = strtol (entry -> d_name , NULL , 10 );
927+ bool root_entry = pid == (long )process -> pid ;
928+ cbm_linux_stat_t status =
929+ cbm_linux_proc_stat (entry -> d_name , root_entry , & parent , & group , & pages );
797930 if (status != CBM_LINUX_STAT_OK ) {
798931 failed = root_entry || status != CBM_LINUX_STAT_GONE ;
799932 continue ;
800933 }
801- if (group != process -> pgid || pages < 0 ) {
934+ if (pages < 0 ) {
802935 continue ;
803936 }
804937 uint64_t page_count = (uint64_t )pages ;
805- uint64_t bytes = page_count > UINT64_MAX / (uint64_t )page_size
806- ? UINT64_MAX
807- : page_count * (uint64_t )page_size ;
808- cbm_rss_add_saturated (& total , bytes );
809- measured ++ ;
938+ cbm_linux_proc_t item = {.pid = (pid_t )pid ,
939+ .parent = parent ,
940+ .group = group ,
941+ .bytes = page_count > UINT64_MAX / (uint64_t )page_size
942+ ? UINT64_MAX
943+ : page_count * (uint64_t )page_size };
944+ failed = !cbm_linux_probe_push (& procs , & count , & capacity , & item );
810945 }
811946 (void )closedir (proc );
812947 if (failed ) {
948+ cbm_free (CBM_MEM_CLASS_OTHER , procs );
813949 return CBM_PROC_TREE_RSS_ERROR ;
814950 }
951+
952+ cbm_linux_probe_mark_tree (procs , count , process -> pgid );
953+ uint64_t total = 0 ;
954+ int measured = 0 ;
955+ for (size_t index = 0 ; index < count ; index ++ ) {
956+ if (procs [index ].counted ) {
957+ cbm_rss_add_saturated (& total , procs [index ].bytes );
958+ measured ++ ;
959+ }
960+ }
961+ cbm_free (CBM_MEM_CLASS_OTHER , procs );
815962 if (measured == 0 ) {
816963 return CBM_PROC_TREE_RSS_EMPTY ;
817964 }
0 commit comments