xref: /xnu-12377.41.6/tools/lldbmacros/recount.py (revision bbb1b6f9e71b8cdde6e5cd6f4841f207dee3d828)
1from xnu import (
2    kern,
3    ArgumentError,
4    unsigned,
5    lldb_command,
6    header,
7    GetEnumValue,
8    GetEnumValues,
9    GetEnumName,
10    GetThreadName,
11    GetProcStartAbsTimeForTask,
12    GetRecentTimestamp,
13    GetProcNameForTask,
14    FindTasksByName,
15    IterateQueue,
16)
17
18
19def validate_args(opts, valid_flags):
20    valid_flags = set(valid_flags)
21    for k in opts.keys():
22        if k[1:] not in valid_flags:
23            raise ArgumentError("-{} not supported in subcommand".format(k))
24
25
26@lldb_command("recount", "AF:MT", fancy=True)
27def Recount(cmd_args=None, cmd_options={}, O=None):  # noqa: E741
28    """Inspect counters maintained by the Recount subsystem on various resource
29    aggregators, like tasks or threads.
30
31    recount task [-TM] <task_t> [...] | -F <task_name>
32    recount thread [-M] <thread_t> [...]
33    recount coalition [-M] <coalition_t> [...]
34    recount processor [-ATM] [<processor_t-or-cpu-id>] [...]
35
36    Options:
37        -T : break out active threads for a task or processor
38        -M : show times in the Mach timebase
39        -A : show all processors
40
41    Diagnostic macros:
42        recount diagnose task <task_t>
43            - Ensure resource accounting consistency in a task.
44        recount triage
45            - Print out statistics useful for general panic triage.
46
47    """
48    if cmd_args is None or len(cmd_args) == 0:
49        raise ArgumentError("subcommand required")
50
51    if cmd_args[0] == "coalition":
52        validate_args(cmd_options, ["M"])
53        RecountCoalition(cmd_args[1:], cmd_options=cmd_options, O=O)
54    elif cmd_args[0] == "task":
55        validate_args(cmd_options, ["F", "M", "T"])
56        RecountTask(cmd_args[1:], cmd_options=cmd_options, O=O)
57    elif cmd_args[0] == "thread":
58        validate_args(cmd_options, ["M"])
59        RecountThread(cmd_args[1:], cmd_options=cmd_options, O=O)
60    elif cmd_args[0] == "processor":
61        validate_args(cmd_options, ["A", "M", "T"])
62        RecountProcessor(cmd_args[1:], cmd_options=cmd_options, O=O)
63    elif cmd_args[0] == "diagnose":
64        RecountDiagnose(cmd_args[1:], cmd_options=cmd_options, O=O)
65    elif cmd_args[0] == "triage":
66        validate_args(cmd_options, [])
67        RecountTriage(cmd_options=cmd_options, O=O)
68    else:
69        raise ArgumentError("{}: invalid subcommand".format(cmd_args[0]))
70
71
72def scale_suffix(val, unit=""):
73    si_units = [
74        (1e21, "Z"),
75        (1e18, "E"),
76        (1e15, "P"),
77        (1e12, "T"),
78        (1e9, "B"),
79        (1e6, "M"),
80        (1e3, "k"),
81        (1, " "),
82        (1e-3, "m"),
83        (1e-6, "u"),
84        (1e-9, "n"),
85    ]
86    scale, sfx = (1, "")
87    for si_scale, si_sfx in si_units:
88        if val >= si_scale:
89            scale, sfx = (si_scale, si_sfx)
90            break
91    return "{:>7.3f}{:<1s}{}".format(val / scale, sfx, unit)
92
93
94class RecountSum(object):
95    """
96    Accumulate usage counters.
97    """
98
99    def __init__(self, mach_times=False):
100        self._mach_times = mach_times
101        self._levels = RecountPlan.levels()
102        self._times_mach = [0] * len(self._levels)
103        self._instructions = [0] * len(self._levels)
104        self._cycles = [0] * len(self._levels)
105        self._energy_nj = 0
106        self._valid_count = 0
107
108    def add_usage(self, usage):
109        for _, level in self._levels:
110            metrics = usage.ru_metrics[level]
111            self._times_mach[level] += unsigned(metrics.rm_time_mach)
112            if hasattr(metrics, "rm_cycles"):
113                self._instructions[level] += unsigned(metrics.rm_instructions)
114                self._cycles[level] += unsigned(metrics.rm_cycles)
115                if unsigned(metrics.rm_cycles) != 0:
116                    self._valid_count += 1
117        if hasattr(usage, "ru_energy_nj"):
118            self._energy_nj += unsigned(usage.ru_energy_nj)
119
120    def user_sys_times(self):
121        user_level = GetEnumValue("recount_level_t", "RCT_LVL_USER")
122        user_time = self._times_mach[user_level]
123        return (user_time, sum(self._times_mach) - user_time)
124
125    def div_valid(self, numer, denom):
126        if self._valid_count == 0 or denom == 0:
127            return 0
128        return numer / denom
129
130    def _convert_time(self, time):
131        if self._mach_times:
132            return time
133        return kern.GetNanotimeFromAbstime(time) / 1e9
134
135    def time(self):
136        time = sum(self._times_mach)
137        if self._mach_times:
138            return time
139        return kern.GetNanotimeFromAbstime(time)
140
141    def fmt_args(self):
142        level_args = [
143            [
144                level_name,
145                self._convert_time(self._times_mach[level]),
146                scale_suffix(self._cycles[level]),
147                self.div_valid(
148                    self._cycles[level],
149                    kern.GetNanotimeFromAbstime(self._times_mach[level]),
150                ),
151                scale_suffix(self._instructions[level]),
152                self.div_valid(self._cycles[level], self._instructions[level]),
153                "-",
154                "-",
155            ]
156            for (level_name, level) in RecountPlan.levels()
157        ]
158
159        total_time_ns = kern.GetNanotimeFromAbstime(sum(self._times_mach))
160        total_cycles = sum(self._cycles)
161        total_insns = sum(self._instructions)
162        power_w = self._energy_nj / total_time_ns if total_time_ns != 0 else 0
163        level_args.append(
164            [
165                "*",
166                total_time_ns / 1e9,
167                scale_suffix(total_cycles),
168                self.div_valid(total_cycles, total_time_ns),
169                scale_suffix(total_insns),
170                self.div_valid(total_cycles, total_insns),
171                scale_suffix(self._energy_nj / 1e9, "J"),
172                scale_suffix(power_w, "W"),
173            ]
174        )
175        return level_args
176
177    def fmt_basic_args(self):
178        return [
179            [
180                level_name,
181                self._convert_time(self._times_mach[level]),
182                self._cycles[level],
183                self._instructions[level],
184                "-",
185            ]
186            for (level_name, level) in RecountPlan.levels()
187        ]
188
189
190class RecountPlan(object):
191    """
192    Format tracks and usage according to a plan.
193    """
194
195    def __init__(self, name, mach_times=False):
196        self._mach_times = mach_times
197        self._group_names = []
198        self._group_column = None
199
200        plan = kern.GetGlobalVariable("recount_" + name + "_plan")
201        topo = plan.rpl_topo
202        if topo == GetEnumValue("recount_topo_t", "RCT_TOPO_CPU"):
203            self._group_column = "cpu"
204            self._group_count = unsigned(kern.globals.real_ncpus)
205            self._group_names = ["cpu-{}".format(i) for i in range(self._group_count)]
206        elif topo == GetEnumValue("recount_topo_t", "RCT_TOPO_CPU_KIND"):
207            if kern.arch.startswith("arm64"):
208                self._group_column = "cpu-kind"
209                cluster_mask = int(kern.globals.topology_info.cluster_types)
210                self._group_count = bin(cluster_mask).count("1")
211                self._group_names = [
212                    GetEnumName("recount_cpu_kind_t", i)[8:][:4]
213                    for i in range(self._group_count)
214                ]
215            else:
216                self._group_count = 1
217        elif topo == GetEnumValue("recount_topo_t", "RCT_TOPO_SYSTEM"):
218            self._group_count = 1
219        else:
220            raise RuntimeError("{}: Unexpected recount topography", topo)
221
222    def time_fmt(self):
223        return "{:>12d}" if self._mach_times else "{:>12.05f}"
224
225    def _usage_fmt(self):
226        prefix = "{n}{{:>6s}} {t} ".format(
227            t=self.time_fmt(), n="{:>8s} " if self._group_column else ""
228        )
229        return prefix + "{:>8s} {:>7.3g} {:>8s} {:>5.03f} {:>9s} {:>9s}"
230
231    def usages(self, usages):
232        for i in range(self._group_count):
233            yield usages[i]
234
235    def track_usages(self, tracks):
236        for i in range(self._group_count):
237            yield tracks[i].rt_usage
238
239    def usage_header(self):
240        fmt = "{:>6s} {:>12s} {:>8s} {:>7s} {:>8s} {:>5s} {:>9s} {:>9s}".format(  # noqa: E501
241            "level",
242            "time",
243            "cycles",
244            "GHz",
245            "insns",
246            "CPI",
247            "energy",
248            "power",
249        )
250        if self._group_column:
251            fmt = "{:>8s} ".format(self._group_column) + fmt
252        return fmt
253
254    def levels():
255        names = ["kernel", "user"]
256        levels = list(
257            zip(
258                names,
259                GetEnumValues(
260                    "recount_level_t", ["RCT_LVL_" + name.upper() for name in names]
261                ),
262            )
263        )
264        try:
265            levels.append(("secure", GetEnumValue("recount_level_t", "RCT_LVL_SECURE")))
266        except KeyError:
267            # RCT_LVL_SECURE is not defined on this system.
268            pass
269        return levels
270
271    def format_usage(self, usage, name=None, sum=None, O=None):
272        rows = []
273
274        levels = RecountPlan.levels()
275        total_time = 0
276        total_time_ns = 0
277        total_cycles = 0
278        total_insns = 0
279        for level_name, level in levels:
280            metrics = usage.ru_metrics[level]
281            time = unsigned(metrics.rm_time_mach)
282            time_ns = kern.GetNanotimeFromAbstime(time)
283            total_time_ns += time_ns
284            if not self._mach_times:
285                time = time_ns / 1e9
286            total_time += time
287            if hasattr(metrics, "rm_cycles"):
288                cycles = unsigned(metrics.rm_cycles)
289                total_cycles += cycles
290                freq = cycles / time_ns if time_ns != 0 else 0
291                insns = unsigned(metrics.rm_instructions)
292                total_insns += insns
293                cpi = cycles / insns if insns != 0 else 0
294            else:
295                cycles = 0
296                freq = 0
297                insns = 0
298                cpi = 0
299            rows.append(
300                [
301                    level_name,
302                    time,
303                    scale_suffix(cycles),
304                    freq,
305                    scale_suffix(insns),
306                    cpi,
307                    "-",
308                    "-",
309                ]
310            )
311
312        if hasattr(usage, "ru_energy_nj"):
313            energy_nj = unsigned(usage.ru_energy_nj)
314            if total_time_ns != 0:
315                power_w = energy_nj / total_time_ns
316            else:
317                power_w = 0
318        else:
319            energy_nj = 0
320            power_w = 0
321        if total_insns != 0:
322            total_freq = total_cycles / total_time_ns if total_time_ns != 0 else 0
323            total_cpi = total_cycles / total_insns
324        else:
325            total_freq = 0
326            total_cpi = 0
327
328        rows.append(
329            [
330                "*",
331                total_time,
332                scale_suffix(total_cycles),
333                total_freq,
334                scale_suffix(total_insns),
335                total_cpi,
336                scale_suffix(energy_nj / 1e9, "J"),
337                scale_suffix(power_w, "W"),
338            ]
339        )
340
341        if sum:
342            sum.add_usage(usage)
343
344        if self._group_column:
345            for row in rows:
346                row.insert(0, name)
347
348        return [O.format(self._usage_fmt(), *row) for row in rows]
349
350    def format_sum(self, sum, O=None):
351        lines = []
352        for line in sum.fmt_args():
353            lines.append(O.format(self._usage_fmt(), "*", *line))
354        return lines
355
356    def format_usages(self, usages, O=None):  # noqa: E741
357        sum = RecountSum(self._mach_times) if self._group_count > 1 else None
358        str = ""
359        for i, usage in enumerate(self.usages(usages)):
360            name = self._group_names[i] if i < len(self._group_names) else None
361            lines = self.format_usage(usage, name=name, sum=sum, O=O)
362            str += "\n".join(lines) + "\n"
363        if sum:
364            str += "\n".join(self.format_sum(sum, O=O))
365        return str
366
367    def format_tracks(self, tracks, O=None):  # noqa: E741
368        sum = RecountSum(self._mach_times) if self._group_count > 1 else None
369        str = ""
370        for i, usage in enumerate(self.track_usages(tracks)):
371            name = self._group_names[i] if i < len(self._group_names) else None
372            lines = self.format_usage(usage, name=name, sum=sum, O=O)
373            str += "\n".join(lines) + "\n"
374        if sum:
375            str += "\n".join(self.format_sum(sum, O=O))
376        return str
377
378    def sum_usages(self, usages, sum=None):
379        if sum is None:
380            sum = RecountSum(mach_times=self._mach_times)
381        for usage in self.usages(usages):
382            sum.add_usage(usage)
383        return sum
384
385    def sum_tracks(self, tracks, sum=None):
386        if sum is None:
387            sum = RecountSum(mach_times=self._mach_times)
388        for usage in self.track_usages(tracks):
389            sum.add_usage(usage)
390        return sum
391
392
393def GetTaskTerminatedUserSysTime(task):
394    plan = RecountPlan("task_terminated")
395    sum = RecountSum()
396    for usage in plan.usages(task.tk_recount.rtk_terminated):
397        sum.add_usage(usage)
398    return sum.user_sys_times()
399
400
401def GetThreadUserSysTime(thread):
402    plan = RecountPlan("thread")
403    sum = RecountSum()
404    for usage in plan.track_usages(thread.th_recount.rth_lifetime):
405        sum.add_usage(usage)
406    return sum.user_sys_times()
407
408
409def print_threads(plan, thread_ptrs, indent=False, O=None):  # noqa: E741
410    for thread_ptr in thread_ptrs:
411        thread = kern.GetValueFromAddress(thread_ptr, "thread_t")
412        print(
413            "{}thread 0x{:x} 0x{:x} {}".format(
414                "    " if indent else "",
415                unsigned(thread.thread_id),
416                unsigned(thread),
417                GetThreadName(thread),
418            )
419        )
420        with O.table(plan.usage_header(), indent=indent):
421            print(plan.format_tracks(thread.th_recount.rth_lifetime, O=O))
422
423
424def RecountThread(thread_ptrs, cmd_options={}, indent=False, O=None):  # noqa: E741
425    plan = RecountPlan("thread", mach_times="-M" in cmd_options)
426    print_threads(plan, thread_ptrs, indent=indent, O=O)
427
428
429def get_task_age_ns(task):
430    start_abs = GetProcStartAbsTimeForTask(task)
431    if start_abs is not None:
432        return kern.GetNanotimeFromAbstime(GetRecentTimestamp() - start_abs)
433    return None
434
435
436def print_task_description(task):
437    task_name = GetProcNameForTask(task)
438    task_age_ns = get_task_age_ns(task)
439    if task_age_ns is not None:
440        duration_desc = "{:.3f}s".format(task_age_ns / 1e9)
441    else:
442        duration_desc = "-s"
443    print("task 0x{:x} {} ({} old)".format(unsigned(task), task_name, duration_desc))
444    return task_name
445
446
447def RecountTask(task_ptrs, cmd_options={}, O=None):  # noqa: E741
448    if "-F" in cmd_options:
449        tasks = FindTasksByName(cmd_options["-F"])
450    else:
451        tasks = [kern.GetValueFromAddress(t, "task_t") for t in task_ptrs]
452    mach_times = "-M" in cmd_options
453    plan = RecountPlan("task", mach_times=mach_times)
454    terminated_plan = RecountPlan("task_terminated", mach_times=mach_times)
455    active_threads = "-T" in cmd_options
456    if active_threads:
457        thread_plan = RecountPlan("thread", mach_times=mach_times)
458    for task in tasks:
459        task_name = print_task_description(task)
460        with O.table(plan.usage_header()):
461            print(plan.format_tracks(task.tk_recount.rtk_lifetime, O=O))
462            if active_threads:
463                threads = [
464                    unsigned(t)
465                    for t in IterateQueue(task.threads, "thread *", "task_threads")
466                ]
467                print_threads(thread_plan, threads, indent=True, O=O)
468        print("task (terminated threads) 0x{:x} {}".format(unsigned(task), task_name))
469        with O.table(terminated_plan.usage_header()):
470            print(terminated_plan.format_usages(task.tk_recount.rtk_terminated, O=O))
471
472
473def RecountCoalition(coal_ptrs, cmd_options={}, O=None):  # noqa: E741
474    plan = RecountPlan("coalition", mach_times="-M" in cmd_options)
475    coals = [kern.GetValueFromAddress(c, "coalition_t") for c in coal_ptrs]
476    for coal in coals:
477        print("coalition 0x{:x} {}".format(unsigned(coal), unsigned(coal.id)))
478        with O.table(plan.usage_header()):
479            print(plan.format_usages(coal.r.co_recount.rco_exited, O=O))
480
481
482def get_processor(ptr_or_id):
483    ptr_or_id = unsigned(ptr_or_id)
484    if ptr_or_id < 1024:
485        processor_list = kern.GetGlobalVariable("processor_list")
486        current_processor = processor_list
487        while unsigned(current_processor) > 0:
488            if unsigned(current_processor.cpu_id) == ptr_or_id:
489                return current_processor
490            current_processor = current_processor.processor_list
491        raise ArgumentError("no processor found with CPU ID {}".format(ptr_or_id))
492    else:
493        return kern.GetValueFromAddress(ptr_or_id, "processor_t")
494
495
496def get_all_processors():
497    processors = []
498    processor_list = kern.GetGlobalVariable("processor_list")
499    current_processor = processor_list
500    while unsigned(current_processor) > 0:
501        processors.append(current_processor)
502        current_processor = current_processor.processor_list
503    return sorted(processors, key=lambda p: p.cpu_id)
504
505
506def RecountProcessor(pr_ptrs_or_ids, cmd_options={}, O=None):  # noqa: E741
507    mach_times = "-M" in cmd_options
508    plan = RecountPlan("processor", mach_times=mach_times)
509    if "-A" in cmd_options:
510        prs = get_all_processors()
511    else:
512        prs = [get_processor(p) for p in pr_ptrs_or_ids]
513    active_threads = "-T" in cmd_options
514    if active_threads:
515        thread_plan = RecountPlan("thread", mach_times=mach_times)
516    hdr_prefix = "{:>18s} {:>4s} {:>4s} ".format(
517        "processor",
518        "cpu",
519        "kind",
520    )
521    header_fmt = " {:>12s} {:>12s} {:>8s}"
522    hdr_suffix = header_fmt.format("idle-time", "total-time", "idle-pct")
523    null_suffix = header_fmt.format("-", "-", "-")
524    levels = RecountPlan.levels()
525    with O.table(hdr_prefix + plan.usage_header() + hdr_suffix):
526        for pr in prs:
527            usage = pr.pr_recount.rpr_active.rt_usage
528            idle_time = pr.pr_recount.rpr_idle_time_mach
529            times = [usage.ru_metrics[i].rm_time_mach for (_, i) in levels]
530            total_time = sum(times) + idle_time
531            if not mach_times:
532                idle_time = kern.GetNanotimeFromAbstime(idle_time) / 1e9
533                total_time = kern.GetNanotimeFromAbstime(total_time) / 1e9
534            pset = pr.processor_set
535            cluster_kind = "SMP"
536            if unsigned(pset.pset_cluster_type) != 0:
537                cluster_kind = GetEnumName(
538                    "pset_cluster_type_t", pset.pset_cluster_type, "PSET_AMP_"
539                )
540            prefix = "{:<#018x} {:>4d} {:>4s} ".format(
541                unsigned(pr), pr.cpu_id, cluster_kind
542            )
543            suffix = (
544                " "
545                + plan.time_fmt().format(idle_time)
546                + " "
547                + plan.time_fmt().format(total_time)
548                + " {:>7.2f}%".format(idle_time / total_time * 100)
549            )
550            usage_lines = plan.format_usage(usage, O=O)
551            for i, line in enumerate(usage_lines):
552                line_suffix = null_suffix
553                if i + 1 == len(usage_lines):
554                    line_suffix = suffix
555                O.write(prefix + line + line_suffix + "\n")
556            if active_threads:
557                active_thread = unsigned(pr.active_thread)
558                if active_thread != 0:
559                    print_threads(thread_plan, [active_thread], indent=True, O=O)
560
561
562@header("{:>4s} {:>20s} {:>20s} {:>20s}".format("cpu", "time-mach", "cycles", "insns"))
563def GetRecountSnapshot(cpu, snap, O=None):
564    (insns, cycles) = (0, 0)
565    if hasattr(snap, "rsn_cycles"):
566        (insns, cycles) = (snap.rsn_insns, snap.rsn_cycles)
567    return O.format(
568        "{:4d} {:20d} {:20d} {:20d}", cpu, snap.rsn_time_mach, cycles, insns
569    )
570
571
572def GetRecountProcessorState(pr):
573    state_time = pr.pr_recount.rpr_state_last_abs_time
574    state = state_time >> 63
575    return (
576        pr.pr_recount.rpr_snap,
577        "I" if state == 1 else "A",
578        state_time & ~(0x1 << 63),
579    )
580
581
582@header(
583    "{:>20s} {:>4s} {:>6s} {:>18s} {:>18s} {:>18s} {:>18s} {:>18s}".format(
584        "processor",
585        "cpu",
586        "state",
587        "last-idle-change",
588        "last-user-change",
589        "last-disp",
590        "since-idle-change",
591        "since-user-change",
592    )
593)
594def GetRecountProcessorDiagnostics(pr, cur_time, O=None):
595    (snap, state, time) = GetRecountProcessorState(pr)
596    cpu_id = unsigned(pr.cpu_id)
597    last_usrchg = snap.rsn_time_mach
598    since_usrchg = cur_time - last_usrchg
599    last_disp = "{}{:>d}".format(
600        "*" if cur_time == unsigned(pr.last_dispatch) else "", pr.last_dispatch
601    )
602    return O.format(
603        "{:>#20x} {:4d} {:>6s} {:>18d} {:>18d} {:>18s} {:>18d} {:>18d}",
604        unsigned(pr),
605        cpu_id,
606        state,
607        time,
608        last_usrchg,
609        last_disp,
610        cur_time - time,
611        since_usrchg,
612    )
613
614
615@header(
616    "{:>12s} {:>6s} {:>12s} {:>20s} {:>20s}".format(
617        "group", "level", "time", "cycles", "insns"
618    )
619)
620def RecountDiagnoseTask(task_ptrs, cmd_options={}, O=None):  # noqa: E74
621    if "-F" in cmd_options:
622        tasks = FindTasksByName(cmd_options["-F"])
623    else:
624        tasks = [kern.GetValueFromAddress(t, "task_t") for t in task_ptrs]
625
626    line_fmt = "{:20s} = {:10.3f}"
627    row_fmt = "{:>12s} {:>6s} {:>12.3f} {:>20d} {:>20d}"
628
629    task_plan = RecountPlan("task", mach_times=False)
630    term_plan = RecountPlan("task_terminated", mach_times=False)
631    for task in tasks:
632        print_task_description(task)
633        with O.table(RecountDiagnoseTask.header):
634            task_sum = task_plan.sum_tracks(task.tk_recount.rtk_lifetime)
635            for line in task_sum.fmt_basic_args():
636                line = line[:-1]
637                print(O.format(row_fmt, "task", *line))
638
639            term_sum = term_plan.sum_usages(task.tk_recount.rtk_terminated)
640            for line in term_sum.fmt_basic_args():
641                print(O.format(row_fmt, "terminated", *line))
642            term_sum_ns = term_sum.time()
643
644            threads_sum = RecountSum(mach_times=True)
645            threads_time_mach = threads_sum.time()
646            for thread in IterateQueue(task.threads, "thread *", "task_threads"):
647                usr_time, sys_time = GetThreadUserSysTime(thread)
648                threads_time_mach += usr_time + sys_time
649
650            threads_sum_ns = kern.GetNanotimeFromAbstime(threads_time_mach)
651            print(line_fmt.format("threads CPU", threads_sum_ns / 1e9))
652
653            all_threads_sum_ns = threads_sum_ns + term_sum_ns
654            print(line_fmt.format("all threads CPU", all_threads_sum_ns / 1e9))
655
656            print(line_fmt.format("discrepancy", task_sum.time() - all_threads_sum_ns))
657
658
659def RecountDiagnose(cmd_args=[], cmd_options={}, O=None):  # noqa: E741
660    if cmd_args is None or len(cmd_args) == 0:
661        raise ArgumentError("diagnose subcommand required")
662
663    if cmd_args[0] == "task":
664        validate_args(cmd_options, ["F"])
665        RecountDiagnoseTask(cmd_args[1:], cmd_options=cmd_options, O=O)
666    else:
667        raise ArgumentError("{}: invalid diagnose subcommand".format(cmd_args[0]))
668
669
670def RecountTriage(cmd_options={}, O=None):  # noqa: E741
671    prs = get_all_processors()
672    print("processors")
673    with O.table(GetRecountProcessorDiagnostics.header, indent=True):
674        max_dispatch = max([unsigned(pr.last_dispatch) for pr in prs])
675        for pr in prs:
676            print(GetRecountProcessorDiagnostics(pr, cur_time=max_dispatch, O=O))
677
678    print("snapshots")
679    with O.table(GetRecountSnapshot.header, indent=True):
680        for i, pr in enumerate(prs):
681            print(GetRecountSnapshot(i, pr.pr_recount.rpr_snap, O=O))
682