xv6, line by line
lab 7

Extension labs · lab 7 · Traps and control flow · ★★☆☆☆

A sampling profiler

Where does a program spend its time? A sampling profiler answers without changing the program: every so often, it stops the program, writes down where it was, and lets it go on. After a few hundred samples, the places that come up most are the places where the time goes. In this lab you build one for xv6. A process asks the kernel to sample it, the kernel counts the interrupted program counters in a histogram of address buckets, and a new command, prof grep ..., prints the hottest functions of any program.

The hardware already stops every program ten times a second: the timer interrupt that drives the scheduler. The design questions are about using that interrupt well. Which piece of code sees every tick on every hart, and which register says where the program was? Where should a histogram live, who may touch it while an interrupt handler is updating it, and what happens to it across fork, exec and exit? A program run under the profiler does not know it is being profiled: how do the samples get out? And when you point the same machinery at the kernel itself, what can it see, and what is it structurally unable to see?

The reference solution is eight small commits. It finds the inner loop of grep’s regular expression matcher from 61 samples, and its kernel profile of usertests turns up a surprise about which kernel code a timer-driven profiler can see at all.

Read first: Tour 5: Life of a system call, Tour 8: Traps taken inside the kernel, Tour 11: From a timer tick to a context switch, Tour 12: One scheduler per hart, Tour 21: exit, wait and zombies, Tour 22: exec, Tour 44: One interrupt, three landing sites, Tour 50: noff and intena through a sleep, a yield and an interrupt · The stacks of xv6, Locks and interrupt state

What this lab teaches

  • How a timer interrupt reaches the kernel on every hart, which code runs for it when the hart was in user mode and when it was in the kernel, and what the sepc register holds at that moment.
  • How to attach per-process state with an interesting lifetime to struct proc: what fork, exec, exit and wait each do to it, and which of these places may sleep.
  • When an interrupt handler can update data without a lock, and when it cannot.
  • What a sample count means statistically: how many samples a second a process gets, and how long a run must be before a percentage means something.
  • What a profiler driven by interrupts can and cannot see inside the kernel, and where the time it cannot see ends up.

The reference branch

ext/07-profiler in ShowMeTheStack/xv6-riscv-labs, branched from the frozen commit 06aad25; 8 commits.

git clone https://github.com/ShowMeTheStack/xv6-riscv-labs
cd xv6-riscv-labs
git checkout -b my-profiler 06aad25   # start your own
git diff 06aad25 origin/ext/07-profiler   # only when you want the answer

1. The spec

The system calls.

The command. prof command args... profiles the command’s executable segment and prints where the samples landed, by function (named from the program’s .sym file, which the Makefile now copies into fs.img) and by bucket. prof -k command profiles the kernel’s text while the command runs; prof -k 0xLO 0xHI command only [LO, HI). The report goes to the standard error, so the command’s output can be redirected:

$ prof grep .*.*.*z b1 > o
prof: 72 ticks
prof: grep: 61 samples, 0 outside [0x0, 0xB71), 4-byte buckets
by function:
  48	78%	matchstar
  13	21%	matchhere
hottest buckets:
  42	68%	0x24	matchstar+0x24
[...]

The test. proftest prints one line per check: bad arguments are refused and profile(0, 0, 0) stops; a 30-tick hot loop gets at least 10 samples and at least 90% of the in-range ones fall inside the loop’s function; fork does not pass the profile on and exec keeps it; a child that spins for 20 ticks and exits while profiling leaves its histogram in prof.out; only one kernel profile may exist, and an idle second of the kernel yields at least 10 samples, none outside the profiled range; 20 profiled children leak no page. Everything it reports, it checks. The leak check counts free pages, so run it on an otherwise idle system.

$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 30 in range, 30 in hot()
proftest: most samples in the hot loop: OK
[...]
proftest: no leaks: OK
proftest: ALL OK

Constraints. A process that never calls profile must behave exactly as before, and usertests -q must still print ALL TESTS PASSED on three harts. The sampling path runs at every tick on every hart, so keep it short.

2. Think first

Answer each question in your head (or on paper) before opening a hint. Hints get more specific; the reference answer comes last.

1Where can you catch every tick?

The profiler needs code that runs once per timer interrupt while the profiled process is in user mode, on whichever of the three harts it happens to be running. Where in the kernel is that? And is the global ticks counter a good clock for it?

Check yourself

1warm-upChoose one

A learner samples in clockintr, inside its if (cpuid() == 0) block, next to ticks++. On three harts, a CPU-bound process runs its hot loop for 3 seconds. What does its profile most likely show?

2solidType a number

The timer comparator is set to time + 1000000 and QEMU’s time counts at 10 MHz. A process spins in user mode, alone on a hart, for 3 seconds. About how many samples does a correct profiler take of it?

decimal, 0x hex or 0b binary

2Which register says where the program was?

You are now in the right spot in usertrap. Which value is the address of the user instruction the timer interrupted, why is it exactly that instruction, and can you read it at any time during usertrap?

Check yourself

1solidDecode the bits

gdb stopped at the sample in usertrap (timer interrupt from user mode) and printed sstatus. Decode three of its bits.

Value: 0x200000020

2solidTrue or false, and why

True or false: in usertrap, r_sepc() called right after yield() returns still holds the interrupted user pc of this process.

Why?

3Where does the histogram live, and for how long?

A histogram needs about a thousand counters. Where do you keep it, and what must fork, exec, exit and wait each do to it? Decide each case, then say where the memory is freed.

Check yourself

1solidMatch the pairs

Match each event to what the reference design does with the histogram page.

4Does the sampling path need a lock?

The sample is taken in an interrupt handler, the histogram is shared memory, and there are three harts. Does the increment in the sample path need a spinlock? Who else could be reading or writing the same histogram at that instant?

Check yourself

1solidFill in the machine state

A timer interrupt from user mode has brought proftest into usertrap, which is about to count the sample. Fill in the machine state of that hart.

5How do the samples get out?

prof grep hello README will run grep, which knows nothing about profiling and will never call profget. When grep exits, its histogram must reach prof. Where in the exit/wait sequence can the kernel hand it over, and what rules out the other places?

Check yourself

1deepChoose one

A learner moves the write-out into freeproc (called from kwait). What happens the first time a profiled child is reaped?

6How long must you sample?

prof grep .*.*.*z README runs for about a second and reports matchstar 70%. Run it again and you may see 62% or 87%. How many samples do you need before a percentage is trustworthy to a few points, and how long is that in seconds of the program’s run?

Check yourself

1solidType a number

You want a function’s share, about 50%, to within one standard error of 2 percentage points. How many samples do you need? (Use sqrt(p(1-p)/n).)

decimal, 0x hex or 0b binary

7What would a kernel profile see?

Now point the same idea at the kernel: count the kernel pc at timer interrupts taken in supervisor mode. Where do you take the sample, does it need a lock now, and, most interesting, which kernel code can such a profile never catch, and where will that time show up instead?

Check yourself

1deepChoose all that apply

A kernel profile samples sepc in kerneltrap on timer interrupts. Which of these kernel instructions can never be the sampled pc?

3. Build it

Start.

git checkout -b my-prof 06aad25

Milestones.

  1. The plumbing. Two system-call numbers (23 and 24), the stubs in user/usys.pl, the prototypes in user/user.h, the table entries in kernel/syscall.c, and kernel/prof.h with struct prof. Put the handlers in a new kernel/prof.c (add $K/prof.o to OBJS) and have them return -1. Test: it builds, and a call returns -1.
  2. The page. p->prof, profile allocating and filling in the header, profile(0, 0, 0) freeing, freeproc freeing and clearing. Test: usertests -q (nothing samples yet).
  3. The sample. One if in usertrap's timer branch and the counting function.
  4. profget. A copyout of the page. Now write a test program with a hot loop: profile your own text, spin for a few seconds, profget, print the buckets. You should see about ten samples a second, all in the loop.
  5. Exit. The write-out in kexit. Test: a profiled child exits; the parent reads prof.out.
  6. The command. prof: read the ELF header, choose the shift, fork, profile, exec, wait, read prof.out, print. Copy the .sym files into fs.img and name the buckets. Inputs must run for seconds, not milliseconds: README is grepped in less than one tick. Build a bigger file inside xv6 with cat README README ... > b1, and use a pattern that makes grep’s matcher backtrack (.*.*.*z).
  7. Kernel mode (optional): a sample in kerneltrap, a lock, one owner at a time.

After each milestone: usertests -q on three harts.

Debugging. Run QEMU with -smp 3, as make qemu does. To see a sample being taken: start QEMU halted with make qemu-gdb (it adds -S and a gdb port of its own, which it writes into .gdbinit), run ${TOOLPREFIX}gdb kernel/kernel in another terminal, break profsample (or your name for it) before the first continue, run your test, then bt, p/x $sepc, p/x $sstatus and p cpus[$tp].noff. If a process gets no samples, check which hart it runs on (info threads). If the kernel faults in your sample function, print the computed index: a pc outside the range is the usual cause (clinic 3). To check for leaks, count free pages before and after with an sbrk loop, on an otherwise idle system. TOOLPREFIX is your RISC-V toolchain’s prefix, the same one xv6’s Makefile detects (riscv64-unknown-elf-, riscv64-linux-gnu- or riscv64-elf-); set it with export TOOLPREFIX=riscv64-unknown-elf- or whichever you have. On Debian/Ubuntu/WSL, gdb-multiarch also works as the debugger.

4. Debugging clinic

Each of these bugs was put into the reference solution on purpose and run on three harts. The symptom is exactly what happened. Try to explain it before revealing why.

1Sampled on hart 0’s tick, in clockintr

The sample moves from usertrap into clockintr, next to ticks++. The learner even checks that the interrupt came from user mode:

   if (cpuid() == 0) {
     acquire(&tickslock);
     ticks++;
+    // a tick: sample the running process, if it was in user mode.
+    struct proc *p = myproc();
+    if (p && p->prof && p->prof->lo < KERNBASE &&
+        (r_sstatus() & SSTATUS_SPP) == 0)
+      profsample(p->prof, r_sepc());
     wakeup(&ticks);
     release(&tickslock);
   }

and the profsample call in usertrap is gone.

What happened when we ran it

$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 30 in range, 30 in hot()
proftest: most samples in the hot loop: OK
[...]
proftest: ALL OK
$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 0 samples, 0 in range, 0 in hot()
proftest: most samples in the hot loop: FAIL
[...]
proftest: 1 FAILED
$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 11 samples, 11 in range, 11 in hot()
proftest: most samples in the hot loop: OK
proftest: fork does not inherit the profile: OK
proftest: exec keeps the profile: OK
proftest: prof.out: 0 samples, 0 in range, 0 in hot()
proftest: exit writes prof.out: FAIL
[...]
proftest: 1 FAILED
$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 0 samples, 0 in range, 0 in hot()
proftest: most samples in the hot loop: FAIL
[...]
proftest: prof.out: 0 samples, 0 in range, 0 in hot()
proftest: exit writes prof.out: FAIL
[...]
proftest: 2 FAILED

2Read sepc after yield

The sample is taken after yield(), from the live register:

   if (which_dev == 2) {
-    // first record where the program was. (a kernel
-    // profile is fed by kerneltrap instead.)
-    if (p->prof && p->prof->lo < KERNBASE)
-      profsample(p->prof, p->trapframe->epc);
     yield();
+    // record where the program was.
+    if (p->prof && p->prof->lo < KERNBASE)
+      profsample(p->prof, r_sepc());
   }

What happened when we ran it

$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 30 in range, 30 in hot()
proftest: most samples in the hot loop: OK
[...]
proftest: ALL OK
$ grep .*.*.*z b1 > o1 &
$ grep .*.*.*z b1 > o2 &
$ grep .*.*.*z b1 > o3 &
$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 22 in range, 12 in hot()
proftest: most samples in the hot loop: FAIL
proftest: fork does not inherit the profile: OK
proftest: exec keeps the profile: OK
proftest: prof.out: 10 samples, 9 in range, 5 in hot()
proftest: exit writes prof.out: FAIL
[...]
proftest: free pages 32440 before, 32470 after
proftest: no leaks: FAIL
proftest: 3 FAILED

3No range check in profsample

The learner trusts the range and drops the test:

 pr->nsample++;
-  if (pc < pr->lo || pc >= pr->hi) {
-    pr->nout++;
-    return;
-  }
 pr->bucket[(pc - pr->lo) >> pr->shift]++;

With a user profile made by prof this changes nothing: prof profiles the whole executable segment, and a user program’s pc is always in it. The bug shows with a kernel profile of part of the kernel, where most samples are outside the range.

What happened when we ran it

$ prof -k 0x80000b00 0x80000d00 usertests -q
usertests starting
[...]
ALL TESTS PASSED
prof: 1207 ticks
prof: kernel: 3565 samples, 0 outside [0x80000B00, 0x80000D00), 2-byte buckets
by function:
  223	6%	pop_off
  14	0%	memset
hottest buckets:
  223	6%	0x80000C50	pop_off+0x28
  14	0%	0x80000CBC	memset+0x14
$ proftest
[...]
proftest: ALL OK

(another boot, a range with 8-byte buckets:)
$ prof -k 0x80001000 0x80002f00 ls
.              1 1 1024
..             1 1 1024
README         2 2 2441
[...]
dorphan.sym   scause=0xd sepc=0x8000577a stval=0x8000000087f04e48
panic: kerneltrap

4Freed the histogram on exec, not on exit

The learner reasons that a profile belongs to a program, so exec should end it, and leaves freeproc alone:

 // kernel/exec.c, kexec, after the old image is freed
   proc_freepagetable(oldpagetable, oldsz);
+
+  // the profile belonged to the old program.
+  if (p->prof) {
+    kfree((void *)p->prof);
+    p->prof = 0;
+  }

 // kernel/proc.c, freeproc
-  if (p->prof)
-    kfree((void *)p->prof);
-  p->prof = 0;

What happened when we ran it

$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 30 in range, 29 in hot()
proftest: most samples in the hot loop: OK
proftest: fork does not inherit the profile: OK
proftest: exec keeps the profile: FAIL
proftest: prof.out: 10 samples, 10 in range, 10 in hot()
proftest: exit writes prof.out: OK
proftest: only one kernel profile at a time: OK
proftest: kernel: 30 samples, 0 outside [0x80000000, 0x8000FE00)
proftest: kernel samples, all in the kernel's text: OK
proftest: free pages 32470 before, 32469 after
proftest: no leaks: FAIL
proftest: 2 FAILED
$ prof grep .*.*.*z README > o
prof: 9 ticks
prof: no prof.out

(another boot, same kernel: gdb attached after proftest finished)
s_sstatus (x=2) at kernel/riscv.h:67
warning: 67	kernel/riscv.h: No such file or directory
proc[0] pid 1 state 2 prof (nil) name init
proc[1] pid 2 state 2 prof (nil) name sh
proc[2] pid 0 state 0 prof (nil) name 
proc[3] pid 0 state 0 prof 0x8002e000 name 
proc[4] pid 0 state 0 prof (nil) name 
[...]

(the same mistake, another fresh boot:)
$ prof -k echo hi
hi
prof: 0 ticks
prof: no prof.out
$ prof -k echo hi
prof: profile failed
prof: 0 ticks
prof: no prof.out
$ proftest
[...]
proftest: a kernel profile: FAIL
[...]
$ usertests -q
[...]
ALL TESTS PASSED
$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 29 samples, 29 in range, 29 in hot()
proftest: most samples in the hot loop: OK
proftest: fork does not inherit the profile: FAIL
proftest: exec keeps the profile: FAIL
[...]

5Wrote prof.out from freeproc

The learner keeps all the profile’s end-of-life code together, in freeproc:

 // kexit
-  if (p->prof)
-    profdump(p);

 // freeproc
-  if (p->prof)
+  if (p->prof) {
+    profdump(p); // save the profile before freeing it
     kfree((void *)p->prof);
+  }

What happened when we ran it

$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 29 samples, 29 in range, 29 in hot()
proftest: most samples in the hot loop: OK
proftest: fork does not inherit the profile: OK
panic: acquire

(another boot, gdb attached with a breakpoint on panic:)
Thread 2 hit Breakpoint 1, panic (s=s@entry=0x80007048 "acquire") at kernel/printk.c:139
[...]
#0  panic (s=s@entry=0x80007048 "acquire") at kernel/printk.c:139
#1  0x0000000080000c28 in acquire (lk=lk@entry=0x80010240 <proc+1104>) at kernel/spinlock.c:26
#2  0x0000000080001fd8 in wakeup (chan=chan@entry=0x8001e110 <itable+40>) at kernel/proc.c:586
#3  0x000000008000406a in releasesleep (lk=lk@entry=0x8001e110 <itable+40>) at kernel/sleeplock.c:42
#4  0x0000000080003340 in iunlock (ip=ip@entry=0x8001e100 <itable+24>) at kernel/fs.c:328
#5  0x0000000080003988 in namex (path=0x80007640 "", path@entry=0x80007638 "prof.out", nameiparent=nameiparent@entry=1, name=name@entry=0x3fffff9ec0 "prof.out") at kernel/fs.c:713
#6  0x0000000080003b20 in nameiparent (path=path@entry=0x80007638 "prof.out", name=name@entry=0x3fffff9ec0 "prof.out") at kernel/fs.c:740
#7  0x000000008000503a in create (path=path@entry=0x80007638 "prof.out", type=type@entry=2, major=major@entry=0, minor=minor@entry=0) at kernel/sysfile.c:264
#8  0x0000000080005976 in profdump (p=p@entry=0x80010240 <proc+1104>) at kernel/prof.c:145
#9  0x0000000080001b00 in freeproc (p=p@entry=0x80010240 <proc+1104>) at kernel/proc.c:162
#10 0x0000000080002220 in kwait (addr=24492) at kernel/proc.c:403
#11 0x00000000800029bc in sys_wait () at kernel/sysproc.c:36
#12 0x000000008000292a in syscall () at kernel/syscall.c:150
#13 0x000000008000268c in usertrap () at kernel/trap.c:69
#14 0x0000003ffffff09c in ?? ()
$1 = (void *) 0x1
$2 = 4
$3 = 1
$4 = 3
$5 = "proftest\000\000\000\000\000\000\000"
[...]

5. The reference solution

Take the guided tour through the reference solution, one commit at a time, with the machine state at every step:

Open the reveal tour →

Or read the commits

  1. 996b88e Add the profile and profget system calls

    Makefile

    @@ -27,8 +27,9 @@ OBJS = \
    2727 $K/exec.o \
    2828 $K/sysfile.o \
    2929 $K/kernelvec.o \
    3030 $K/plic.o \
    31 $K/prof.o \
    3132 $K/virtio_disk.o
    3233
    3334# riscv64-unknown-elf- or riscv64-linux-gnu-
    3435# perhaps in /opt/riscv/bin

    kernel/prof.c

    @@ -0,0 +1,24 @@
    1// A sampling profiler: on each timer interrupt, record
    2// where the interrupted program was.
    3
    4#include "types.h"
    5#include "param.h"
    6#include "riscv.h"
    7#include "spinlock.h"
    8#include "proc.h"
    9#include "prof.h"
    10#include "defs.h"
    11
    12// profile(lo, hi, shift): start sampling the calling process.
    13uint64
    14sys_profile(void)
    15{
    16 return -1;
    17}
    18
    19// profget(buf): copy the calling process's histogram to buf.
    20uint64
    21sys_profget(void)
    22{
    23 return -1;
    24}

    kernel/prof.h

    @@ -0,0 +1,14 @@
    1// A sampling profiler's histogram: one page.
    2// Bucket i counts samples whose pc was in
    3// [lo + (i << shift), lo + ((i+1) << shift)).
    4
    5#define PROFBUCKETS 1016
    6
    7struct prof {
    8 uint64 lo, hi; // pcs in [lo, hi) are counted in buckets
    9 int shift; // each bucket covers 1 << shift bytes
    10 int nbucket; // buckets in use
    11 uint nsample; // samples taken
    12 uint nout; // samples whose pc was outside [lo, hi)
    13 uint bucket[PROFBUCKETS];
    14};

    kernel/syscall.c

    @@ -102,8 +102,10 @@ extern uint64 sys_unlink(void);
    102102extern uint64 sys_link(void);
    103103extern uint64 sys_mkdir(void);
    104104extern uint64 sys_close(void);
    105105extern uint64 sys_sync(void);
    106extern uint64 sys_profile(void);
    107extern uint64 sys_profget(void);
    106108
    107109// An array mapping syscall numbers from syscall.h
    108110// to the function that handles the system call.
    109111static uint64 (*syscalls[])(void) = {
    @@ -129,8 +131,10 @@ static uint64 (*syscalls[])(void) = {
    129131 [SYS_link] = sys_link,
    130132 [SYS_mkdir] = sys_mkdir,
    131133 [SYS_close] = sys_close,
    132134 [SYS_sync] = sys_sync,
    135 [SYS_profile] = sys_profile,
    136 [SYS_profget] = sys_profget,
    133137 // clang-format on
    134138};
    135139
    136140void

    kernel/syscall.h

    @@ -20,4 +20,6 @@
    2020#define SYS_link 19
    2121#define SYS_mkdir 20
    2222#define SYS_close 21
    2323#define SYS_sync 22
    24#define SYS_profile 23
    25#define SYS_profget 24

    user/user.h

    @@ -1,7 +1,8 @@
    11#define SBRK_ERROR ((char *)-1)
    22
    33struct stat;
    4struct prof;
    45
    56// system calls
    67int fork(void);
    78int exit(int) __attribute__((noreturn));
    @@ -24,8 +25,10 @@ int getpid(void);
    2425char *sys_sbrk(int, int);
    2526int pause(int);
    2627int uptime(void);
    2728int sync(void);
    29int profile(uint64, uint64, int);
    30int profget(struct prof *);
    2831
    2932// ulib.c
    3033int stat(const char *, struct stat *);
    3134char *strcpy(char *, const char *);

    user/usys.pl

    @@ -42,4 +42,6 @@ entry("getpid");
    4242entry("sbrk");
    4343entry("pause");
    4444entry("uptime");
    4545entry("sync");
    46entry("profile");
    47entry("profget");
  2. 68c63f4 Give each profiled process a histogram page

    kernel/proc.c

    @@ -157,8 +157,11 @@ freeproc(struct proc *p)
    157157{
    158158 if (p->trapframe)
    159159 kfree((void *)p->trapframe);
    160160 p->trapframe = 0;
    161 if (p->prof)
    162 kfree((void *)p->prof);
    163 p->prof = 0;
    161164 if (p->pagetable)
    162165 proc_freepagetable(p->pagetable, p->sz);
    163166 p->pagetable = 0;
    164167 p->sz = 0;

    kernel/proc.h

    @@ -100,5 +100,6 @@ struct proc {
    100100 struct context context; // swtch() here to run process
    101101 struct file *ofile[NOFILE]; // Open files
    102102 struct inode *cwd; // Current directory
    103103 char name[16]; // Process name (debugging)
    104 struct prof *prof; // sampling profile, or 0
    104105};

    kernel/prof.c

    @@ -8,13 +8,43 @@
    88#include "proc.h"
    99#include "prof.h"
    1010#include "defs.h"
    1111
    12// profile(lo, hi, shift): start sampling the calling process.
    12_Static_assert(sizeof(struct prof) == PGSIZE, "struct prof must fill a page");
    13
    14// profile(lo, hi, shift): start sampling the calling process
    15// into a new, empty histogram whose buckets cover [lo, hi),
    16// 1 << shift bytes each. profile(0, 0, 0) stops sampling.
    17// either way, the old histogram is discarded.
    1318uint64
    1419sys_profile(void)
    1520{
    16 return -1;
    21 uint64 lo, hi;
    22 int shift;
    23 struct prof *pr = 0;
    24 struct proc *p = myproc();
    25
    26 argaddr(0, &lo);
    27 argaddr(1, &hi);
    28 argint(2, &shift);
    29
    30 if (lo != 0 || hi != 0) {
    31 if (lo >= hi || shift < 0 || shift > 31 ||
    32 ((hi - lo - 1) >> shift) >= PROFBUCKETS)
    33 return -1;
    34 if ((pr = (struct prof *)kalloc()) == 0)
    35 return -1;
    36 memset(pr, 0, PGSIZE);
    37 pr->lo = lo;
    38 pr->hi = hi;
    39 pr->shift = shift;
    40 pr->nbucket = ((hi - lo - 1) >> shift) + 1;
    41 }
    42
    43 if (p->prof)
    44 kfree((void *)p->prof);
    45 p->prof = pr;
    46 return 0;
    1747}
    1848
    1949// profget(buf): copy the calling process's histogram to buf.
    2050uint64
  3. 2af530d Sample the user pc on every timer interrupt

    kernel/defs.h

    @@ -4,8 +4,9 @@ struct context;
    44struct file;
    55struct inode;
    66struct pipe;
    77struct proc;
    8struct prof;
    89struct spinlock;
    910struct sleeplock;
    1011struct stat;
    1112struct superblock;
    @@ -176,8 +177,11 @@ void plicinit(void);
    176177void plicinithart(void);
    177178int plic_claim(void);
    178179void plic_complete(int);
    179180
    181// prof.c
    182void profsample(struct prof*, uint64);
    183
    180184// virtio_disk.c
    181185void virtio_disk_init(void);
    182186void virtio_disk_rw(struct buf *, int);
    183187void virtio_disk_intr(void);

    kernel/prof.c

    @@ -8,8 +8,20 @@
    88#include "proc.h"
    99#include "prof.h"
    1010#include "defs.h"
    1111
    12// count one sample: the interrupted program was at pc.
    13void
    14profsample(struct prof *pr, uint64 pc)
    15{
    16 pr->nsample++;
    17 if (pc < pr->lo || pc >= pr->hi) {
    18 pr->nout++;
    19 return;
    20 }
    21 pr->bucket[(pc - pr->lo) >> pr->shift]++;
    22}
    23
    1224_Static_assert(sizeof(struct prof) == PGSIZE, "struct prof must fill a page");
    1325
    1426// profile(lo, hi, shift): start sampling the calling process
    1527// into a new, empty histogram whose buckets cover [lo, hi),

    kernel/trap.c

    @@ -81,10 +81,14 @@ usertrap(void)
    8181 if (killed(p))
    8282 kexit(-1);
    8383
    8484 // give up the CPU if this is a timer interrupt.
    85 if (which_dev == 2)
    85 if (which_dev == 2) {
    86 // first record where the program was.
    87 if (p->prof)
    88 profsample(p->prof, p->trapframe->epc);
    8689 yield();
    90 }
    8791
    8993
    9094 // the user page table to switch to, for trampoline.S
  4. e2439ec Copy the histogram out with profget

    kernel/prof.c

    @@ -57,10 +57,18 @@ sys_profile(void)
    5757 p->prof = pr;
    5858 return 0;
    5959}
    6060
    61// profget(buf): copy the calling process's histogram to buf.
    61// profget(buf): copy the calling process's histogram,
    62// a struct prof, to buf.
    6263uint64
    6364sys_profget(void)
    6465{
    65 return -1;
    66 uint64 buf;
    67 struct proc *p = myproc();
    68
    69 argaddr(0, &buf);
    70 if (p->prof == 0)
    71 return -1;
    72 return copyout(p->pagetable, p->sz, buf, (char *)p->prof,
    73 sizeof(struct prof));
    6674}
  5. 3b7d745 Write the profile to prof.out at exit

    kernel/defs.h

    @@ -130,8 +130,11 @@ char* safestrcpy(char*, const char*, int);
    130130int strlen(const char*);
    131131int strncmp(const char*, const char*, uint);
    132132char* strncpy(char*, const char*, int);
    133133
    134// sysfile.c
    135struct inode* create(char*, short, short, short);
    136
    134137// syscall.c
    135138void argint(int, int*);
    136139int argstr(int, char*, int);
    137140void argaddr(int, uint64 *);
    @@ -179,8 +182,9 @@ int plic_claim(void);
    179182void plic_complete(int);
    180183
    181184// prof.c
    182185void profsample(struct prof*, uint64);
    186void profdump(struct proc*);
    183187
    184188// virtio_disk.c
    185189void virtio_disk_init(void);
    186190void virtio_disk_rw(struct buf *, int);

    kernel/proc.c

    @@ -332,8 +332,13 @@ kexit(int status)
    332332
    333333 if (p == initproc)
    334334 panic("init exiting");
    335335
    336 // save the profile, if any, while the process still
    337 // has its current directory.
    338 if (p->prof)
    339 profdump(p);
    340
    336341 // Close all open files.
    337342 for (int fd = 0; fd < NOFILE; fd++) {
    338343 if (p->ofile[fd]) {
    339344 struct file *f = p->ofile[fd];

    kernel/prof.c

    @@ -5,8 +5,9 @@
    55#include "param.h"
    66#include "riscv.h"
    77#include "spinlock.h"
    88#include "proc.h"
    9#include "stat.h"
    910#include "prof.h"
    1011#include "defs.h"
    1112
    1213// count one sample: the interrupted program was at pc.
    @@ -71,4 +72,19 @@ sys_profget(void)
    7172 return -1;
    7273 return copyout(p->pagetable, p->sz, buf, (char *)p->prof,
    7374 sizeof(struct prof));
    7475}
    76
    77// called by kexit: write the histogram to the file prof.out
    78// in the process's current directory.
    79void
    80profdump(struct proc *p)
    81{
    82 struct inode *ip;
    83
    84 begin_op();
    85 if ((ip = create("prof.out", T_FILE, 0, 0)) != 0) {
    86 writei(ip, 0, (uint64)p->prof, 0, sizeof(struct prof));
    87 iunlockput(ip);
    88 }
    89 end_op();
    90}

    kernel/sysfile.c

    @@ -254,9 +254,9 @@ bad:
    254254 end_op();
    255255 return -1;
    256256}
    257257
    258static struct inode *
    258struct inode *
    259259create(char *path, short type, short major, short minor)
    260260{
    261261 struct inode *ip, *dp;
    262262 char name[DIRSIZ];
  6. 2f18bca Add proftest, a test program for the profiler

    Makefile

    @@ -150,8 +150,9 @@ UPROGS=\
    150150 $U/_logstress\
    151151 $U/_forphan\
    152152 $U/_dorphan\
    153153 $U/_sync\
    154 $U/_proftest\
    154155
    155156fs.img: mkfs/mkfs README $(UPROGS)
    156157 mkfs/mkfs fs.img README $(UPROGS)
    157158

    user/proftest.c

    @@ -0,0 +1,205 @@
    1// proftest: checks the profile and profget system calls.
    2
    3#include "kernel/types.h"
    4#include "kernel/fcntl.h"
    5#include "kernel/prof.h"
    6#include "user/user.h"
    7
    8#define PGSIZE 4096
    9
    10static struct prof pr;
    11static int failed;
    12
    13static void
    14check(char *what, int ok)
    15{
    16 printf("proftest: %s: %s\n", what, ok ? "OK" : "FAIL");
    17 if (!ok)
    18 failed++;
    19}
    20
    21// spin in user mode for n clock ticks.
    22__attribute__((noinline)) uint
    23hot(int n)
    24{
    25 uint x = 1;
    26 int t0 = uptime();
    27
    28 while (uptime() - t0 < n)
    29 for (int i = 0; i < 1000000; i++)
    30 x = x * 1103515245 + 12345;
    31 return x;
    32}
    33
    34int hotsamples(void);
    35int hotprofile(char *);
    36int countfree(void);
    37int main(int, char **);
    38
    39// where hot() ends: the compiler need not place functions in
    40// source order, so take the nearest function above it.
    41uint64
    42hotend(void)
    43{
    44 uint64 f[] = {(uint64)check, (uint64)hotend, (uint64)hotsamples,
    45 (uint64)hotprofile, (uint64)countfree, (uint64)main};
    46 uint64 end = PGSIZE;
    47
    48 for (int i = 0; i < sizeof(f) / sizeof(f[0]); i++)
    49 if (f[i] > (uint64)hot && f[i] < end)
    50 end = f[i];
    51 return end;
    52}
    53
    54// how many samples in pr landed inside hot()?
    55int
    56hotsamples(void)
    57{
    58 int n = 0;
    59 uint64 end = hotend();
    60
    61 for (int i = 0; i < pr.nbucket; i++) {
    62 uint64 a = pr.lo + ((uint64)i << pr.shift);
    63 if (a >= (uint64)hot && a < end)
    64 n += pr.bucket[i];
    65 }
    66 return n;
    67}
    68
    69// is pr a good profile of a run of hot()?
    70int
    71hotprofile(char *who)
    72{
    73 int h = hotsamples();
    74 int in = pr.nsample - pr.nout;
    75
    76 printf("proftest: %s: %d samples, %d in range, %d in hot()\n", who,
    77 pr.nsample, in, h);
    78 return pr.nsample >= 10 && h * 10 >= in * 9;
    79}
    80
    81// count free pages, by allocating them all.
    82int
    83countfree(void)
    84{
    85 int n = 0;
    86 char *sz0 = sbrk(0);
    87
    88 while (sbrk(PGSIZE) != SBRK_ERROR)
    89 n++;
    90 sbrk(-(sbrk(0) - sz0));
    91 return n;
    92}
    93
    94void
    95badargs(void)
    96{
    97 int ok = profget(&pr) < 0;
    98 ok = ok && profile(100, 100, 2) < 0; // empty range
    99 ok = ok && profile(0, 0x100000, 2) < 0; // too many buckets
    100 ok = ok && profile(0, PGSIZE, -1) < 0; // bad shift
    101 ok = ok && profile(0, PGSIZE, 3) == 0; // 512 buckets
    102 ok = ok && profget(&pr) == 0 && pr.nbucket == 512;
    103 ok = ok && profile(0, 0, 0) == 0 && profget(&pr) < 0;
    104 check("arguments, and profile(0, 0, 0) stops", ok);
    105}
    106
    107void
    108hotloop(void)
    109{
    110 if (profile(0, PGSIZE, 3) < 0 || hot(30) == 0 || profget(&pr) < 0) {
    111 check("a hot loop", 0);
    112 return;
    113 }
    114 profile(0, 0, 0);
    115 check("most samples in the hot loop", hotprofile("hot loop"));
    116}
    117
    118void
    119forkexec(void)
    120{
    121 int xs = 1;
    122
    123 profile(0, PGSIZE, 3);
    124 if (fork() == 0)
    125 exit(profget(&pr) < 0 ? 0 : 1);
    126 wait(&xs);
    127 check("fork does not inherit the profile", xs == 0);
    128
    129 xs = 1;
    130 if (fork() == 0) {
    131 char *argv[] = {"proftest", "exec", 0};
    132 profile(0, PGSIZE, 3);
    133 exec("proftest", argv);
    134 exit(1);
    135 }
    136 wait(&xs);
    137 check("exec keeps the profile", xs == 0);
    138 profile(0, 0, 0);
    139}
    140
    141void
    142exitdump(void)
    143{
    144 int fd, n, xs = 1;
    145
    146 unlink("prof.out");
    147 if (fork() == 0) {
    148 profile(0, PGSIZE, 3);
    149 hot(20);
    150 exit(0); // no profile(0, 0, 0): kexit writes prof.out
    151 }
    152 wait(&xs);
    153 memset(&pr, 0, sizeof(pr));
    154 if ((fd = open("prof.out", O_RDONLY)) < 0) {
    155 check("exit writes prof.out", 0);
    156 return;
    157 }
    158 n = read(fd, &pr, sizeof(pr));
    159 close(fd);
    160 unlink("prof.out");
    161 check("exit writes prof.out",
    162 xs == 0 && n == sizeof(pr) && pr.lo == 0 && pr.hi == PGSIZE &&
    163 pr.shift == 3 && hotprofile("prof.out"));
    164}
    165
    166void
    167leaks(void)
    168{
    169 int free0 = countfree();
    170
    171 for (int i = 0; i < 20; i++) {
    172 if (fork() == 0) {
    173 profile(0, PGSIZE, 3);
    174 exit(0);
    175 }
    176 wait(0);
    177 }
    178 unlink("prof.out");
    179 int free1 = countfree();
    180 printf("proftest: free pages %d before, %d after\n", free0, free1);
    181 check("no leaks", free0 == free1);
    182}
    183
    184int
    185main(int argc, char *argv[])
    186{
    187 if (argc == 2 && strcmp(argv[1], "exec") == 0) {
    188 // run by forkexec, which called profile(0, 4096, 3) before exec.
    189 exit(profget(&pr) == 0 && pr.lo == 0 && pr.hi == PGSIZE ? 0 : 1);
    190 }
    191 if (hotend() >= PGSIZE) {
    192 printf("proftest: hot() is not where the test expects it\n");
    193 exit(1);
    194 }
    195 badargs();
    196 hotloop();
    197 forkexec();
    198 exitdump();
    199 leaks();
    200 if (failed == 0)
    201 printf("proftest: ALL OK\n");
    202 else
    203 printf("proftest: %d FAILED\n", failed);
    204 exit(0);
    205}
  7. 119df60 Add prof, which runs a command under the profiler

    Makefile

    @@ -151,11 +151,16 @@ UPROGS=\
    151151 $U/_forphan\
    152152 $U/_dorphan\
    153153 $U/_sync\
    154154 $U/_proftest\
    155 $U/_prof\
    155156
    156fs.img: mkfs/mkfs README $(UPROGS)
    157 mkfs/mkfs fs.img README $(UPROGS)
    157# each program's symbol table, for prof. (forktest has none.)
    158SYMS = $(filter-out $U/forktest.sym,$(patsubst $U/_%,$U/%.sym,$(UPROGS)))
    159$U/%.sym: $U/_% ;
    160
    161fs.img: mkfs/mkfs README $(UPROGS) $(SYMS)
    162 mkfs/mkfs fs.img README $(UPROGS) $(SYMS)
    158163
    159164-include kernel/*.d user/*.d
    160165
    161166clean:

    user/prof.c

    @@ -0,0 +1,214 @@
    1// prof: run a command under the sampling profiler, then show
    2// where its samples landed. the report goes to the standard
    3// error, so the command's own output can be redirected.
    4//
    5// prof command args...
    6
    7#include "kernel/types.h"
    8#include "kernel/stat.h"
    9#include "kernel/fcntl.h"
    10#include "kernel/elf.h"
    11#include "kernel/prof.h"
    12#include "user/user.h"
    13
    14#define NSYM 256
    15#define NTOP 10
    16
    17static struct prof pr;
    18static uint64 symaddr[NSYM]; // the program's functions, from NAME.sym
    19static char *symname[NSYM];
    20static int nsym;
    21static uint symcount[NSYM]; // samples per function
    22
    23// find the executable segment [*lo, *hi) in path's ELF headers.
    24int
    25textrange(char *path, uint64 *lo, uint64 *hi)
    26{
    27 uint64 buf[64]; // the first 512 bytes; uint64 for alignment
    28 struct elfhdr *eh = (struct elfhdr *)buf;
    29 struct proghdr *ph;
    30 int fd, n;
    31
    32 if ((fd = open(path, O_RDONLY)) < 0)
    33 return -1;
    34 n = read(fd, buf, sizeof(buf));
    35 close(fd);
    36 if (n < (int)sizeof(*eh) || eh->magic != ELF_MAGIC ||
    37 eh->phoff + eh->phnum * sizeof(*ph) > n)
    38 return -1;
    39 for (int i = 0; i < eh->phnum; i++) {
    40 ph = (struct proghdr *)((char *)buf + eh->phoff) + i;
    41 if (ph->type == ELF_PROG_LOAD && (ph->flags & ELF_PROG_FLAG_EXEC)) {
    42 *lo = ph->vaddr;
    43 *hi = ph->vaddr + ph->memsz;
    44 return 0;
    45 }
    46 }
    47 return -1;
    48}
    49
    50int
    51hexdigit(char c)
    52{
    53 if (c >= '0' && c <= '9')
    54 return c - '0';
    55 if (c >= 'a' && c <= 'f')
    56 return c - 'a' + 10;
    57 return -1;
    58}
    59
    60// read /NAME.sym, written by the Makefile: lines of
    61// "0000000000000058 matchhere". skip sections and files.
    62void
    63loadsyms(char *path)
    64{
    65 char file[32], *s, *buf;
    66 struct stat st;
    67 int fd, n;
    68
    69 s = path + strlen(path);
    70 while (s > path && s[-1] != '/')
    71 s--;
    72 if (strlen(s) + 6 > sizeof(file))
    73 return;
    74 file[0] = '/'; // the Makefile puts them in the root directory
    75 strcpy(file + 1, s);
    76 strcpy(file + strlen(file), ".sym");
    77 if ((fd = open(file, O_RDONLY)) < 0)
    78 return;
    79 if (fstat(fd, &st) < 0 || (buf = malloc(st.size + 1)) == 0) {
    80 close(fd);
    81 return;
    82 }
    83 n = read(fd, buf, st.size);
    84 close(fd);
    85 if (n < 0)
    86 return;
    87 buf[n] = 0;
    88
    89 for (s = buf; *s && nsym < NSYM;) {
    90 uint64 a = 0;
    91 char *name;
    92 while (hexdigit(*s) >= 0)
    93 a = a * 16 + hexdigit(*s++);
    94 if (*s == ' ')
    95 s++;
    96 name = s;
    97 while (*s && *s != '\n')
    98 s++;
    99 if (*s)
    100 *s++ = 0;
    101 if (*name && strchr(name, '.') == 0) {
    102 symaddr[nsym] = a;
    103 symname[nsym++] = name;
    104 }
    105 }
    106}
    107
    108// which function contains a? -1 if none is known.
    109int
    110lookup(uint64 a)
    111{
    112 int best = -1;
    113
    114 for (int i = 0; i < nsym; i++)
    115 if (symaddr[i] <= a && (best < 0 || symaddr[i] > symaddr[best]))
    116 best = i;
    117 return best;
    118}
    119
    120// the index of the largest of n counts not yet printed (used[i] == 0).
    121int
    122biggest(uint *count, char *used, int n)
    123{
    124 int best = -1;
    125
    126 for (int i = 0; i < n; i++)
    127 if (!used[i] && count[i] > 0 && (best < 0 || count[i] > count[best]))
    128 best = i;
    129 if (best >= 0)
    130 used[best] = 1;
    131 return best;
    132}
    133
    134void
    135report(char *cmd)
    136{
    137 static char used[PROFBUCKETS];
    138 int in = pr.nsample - pr.nout;
    139
    140 fprintf(2, "prof: %s: %d samples, %d outside [0x%lx, 0x%lx), %d-byte buckets\n",
    141 cmd, pr.nsample, pr.nout, pr.lo, pr.hi, 1 << pr.shift);
    142 if (in == 0)
    143 return;
    144
    145 if (nsym > 0) {
    146 for (int i = 0; i < pr.nbucket; i++) {
    147 int f = lookup(pr.lo + ((uint64)i << pr.shift));
    148 if (f >= 0)
    149 symcount[f] += pr.bucket[i];
    150 }
    151 fprintf(2, "by function:\n");
    152 for (int k = 0, f; k < NTOP && (f = biggest(symcount, used, nsym)) >= 0;
    153 k++)
    154 fprintf(2, " %d\t%d%%\t%s\n", symcount[f], symcount[f] * 100 / in,
    155 symname[f]);
    156 memset(used, 0, sizeof(used));
    157 }
    158
    159 fprintf(2, "hottest buckets:\n");
    160 for (int k = 0, b; k < NTOP && (b = biggest(pr.bucket, used, pr.nbucket)) >= 0;
    161 k++) {
    162 uint64 a = pr.lo + ((uint64)b << pr.shift);
    163 int f = lookup(a);
    164 fprintf(2, " %d\t%d%%\t0x%lx", pr.bucket[b], pr.bucket[b] * 100 / in, a);
    165 if (f >= 0)
    166 fprintf(2, "\t%s+0x%lx", symname[f], a - symaddr[f]);
    167 fprintf(2, "\n");
    168 }
    169}
    170
    171int
    172main(int argc, char *argv[])
    173{
    174 uint64 lo, hi;
    175 int shift, fd, t0;
    176
    177 if (argc < 2) {
    178 fprintf(2, "usage: prof command args...\n");
    179 exit(1);
    180 }
    181 if (textrange(argv[1], &lo, &hi) < 0) {
    182 fprintf(2, "prof: cannot read %s\n", argv[1]);
    183 exit(1);
    184 }
    185
    186 // the smallest buckets that cover the range; at least 2 bytes,
    187 // the size of the smallest (compressed) instruction.
    188 for (shift = 1; ((hi - lo - 1) >> shift) >= PROFBUCKETS; shift++)
    189 ;
    190
    191 unlink("prof.out");
    192 t0 = uptime();
    193 if (fork() == 0) {
    194 if (profile(lo, hi, shift) < 0) {
    195 fprintf(2, "prof: profile failed\n");
    196 exit(1);
    197 }
    198 exec(argv[1], argv + 1);
    199 fprintf(2, "prof: exec %s failed\n", argv[1]);
    200 exit(1);
    201 }
    202 wait(0);
    203 fprintf(2, "prof: %d ticks\n", uptime() - t0);
    204
    205 if ((fd = open("prof.out", O_RDONLY)) < 0 ||
    206 read(fd, &pr, sizeof(pr)) != sizeof(pr)) {
    207 fprintf(2, "prof: no prof.out\n");
    208 exit(1);
    209 }
    210 close(fd);
    211 loadsyms(argv[1]);
    212 report(argv[1]);
    213 exit(0);
    214}

    user/proftest.c

    @@ -145,9 +145,9 @@ exitdump(void)
    145145
    146146 unlink("prof.out");
    147147 if (fork() == 0) {
    148148 profile(0, PGSIZE, 3);
    149 hot(20);
    149 hot(10);
    150150 exit(0); // no profile(0, 0, 0): kexit writes prof.out
    151151 }
    152152 wait(&xs);
    153153 memset(&pr, 0, sizeof(pr));
  8. 541e7e0 Add a kernel profile, fed by kerneltrap on every hart

    Makefile

    @@ -157,10 +157,14 @@ UPROGS=\
    157157# each program's symbol table, for prof. (forktest has none.)
    158158SYMS = $(filter-out $U/forktest.sym,$(patsubst $U/_%,$U/%.sym,$(UPROGS)))
    159159$U/%.sym: $U/_% ;
    160160
    161fs.img: mkfs/mkfs README $(UPROGS) $(SYMS)
    162 mkfs/mkfs fs.img README $(UPROGS) $(SYMS)
    161# and the kernel's, for prof -k.
    162$U/kernel.sym: $K/kernel
    163 cp $K/kernel.sym $U/kernel.sym
    164
    165fs.img: mkfs/mkfs README $(UPROGS) $(SYMS) $U/kernel.sym
    166 mkfs/mkfs fs.img README $(UPROGS) $(SYMS) $U/kernel.sym
    163167
    164168-include kernel/*.d user/*.d
    165169
    166170clean:

    kernel/defs.h

    @@ -181,9 +181,11 @@ void plicinithart(void);
    181181int plic_claim(void);
    182182void plic_complete(int);
    183183
    184184// prof.c
    185void profinit(void);
    185186void profsample(struct prof*, uint64);
    187void kprofsample(uint64);
    186188void profdump(struct proc*);
    187189
    188190// virtio_disk.c
    189191void virtio_disk_init(void);

    kernel/main.c

    @@ -21,8 +21,9 @@ main()
    2121 kvminithart(); // turn on paging
    2222 procinit(); // process table
    2323 trapinit(); // trap vectors
    2424 trapinithart(); // install kernel trap vector
    25 profinit(); // sampling profiler
    2526 plicinit(); // set up interrupt controller
    2627 plicinithart(); // ask PLIC for device interrupts
    2728 binit(); // buffer cache
    2829 iinit(); // inode table

    kernel/prof.c

    @@ -1,16 +1,32 @@
    11// A sampling profiler: on each timer interrupt, record
    22// where the interrupted program was.
    3//
    4// a user profile (lo < KERNBASE) belongs to one process and is
    5// fed by usertrap, on the hart running that process, so it
    6// needs no lock. a kernel profile (lo >= KERNBASE) is fed by
    7// kerneltrap on every hart, so it has a lock, and there is at
    8// most one at a time.
    39
    410#include "types.h"
    511#include "param.h"
    12#include "memlayout.h"
    613#include "riscv.h"
    714#include "spinlock.h"
    815#include "proc.h"
    916#include "stat.h"
    1017#include "prof.h"
    1118#include "defs.h"
    1219
    20struct spinlock kproflock;
    21struct prof *kprof; // the kernel profile, or 0; kproflock
    22
    23void
    24profinit(void)
    25{
    26 initlock(&kproflock, "kprof");
    27}
    28
    1329// count one sample: the interrupted program was at pc.
    1430void
    1531profsample(struct prof *pr, uint64 pc)
    1632{
    @@ -21,14 +37,37 @@ profsample(struct prof *pr, uint64 pc)
    2137 }
    2238 pr->bucket[(pc - pr->lo) >> pr->shift]++;
    2339}
    2440
    41// called by kerneltrap for each timer interrupt, on every hart:
    42// the kernel was interrupted at pc.
    43void
    44kprofsample(uint64 pc)
    45{
    46 acquire(&kproflock);
    47 if (kprof)
    48 profsample(kprof, pc);
    49 release(&kproflock);
    50}
    51
    52// the process's histogram will no longer be fed:
    53// make sure no hart is still adding to it.
    54static void
    55profdetach(struct proc *p)
    56{
    57 acquire(&kproflock);
    58 if (kprof == p->prof)
    59 kprof = 0;
    60 release(&kproflock);
    61}
    62
    2563_Static_assert(sizeof(struct prof) == PGSIZE, "struct prof must fill a page");
    2664
    27// profile(lo, hi, shift): start sampling the calling process
    28// into a new, empty histogram whose buckets cover [lo, hi),
    29// 1 << shift bytes each. profile(0, 0, 0) stops sampling.
    30// either way, the old histogram is discarded.
    65// profile(lo, hi, shift): start sampling into a new, empty
    66// histogram whose buckets cover [lo, hi), 1 << shift bytes
    67// each: the calling process's user pcs, or, if lo >= KERNBASE,
    68// the kernel's pcs on all harts. profile(0, 0, 0) stops
    69// sampling. either way, the old histogram is discarded.
    3170uint64
    3271sys_profile(void)
    3372{
    3473 uint64 lo, hi;
    @@ -52,10 +91,25 @@ sys_profile(void)
    5291 pr->shift = shift;
    5392 pr->nbucket = ((hi - lo - 1) >> shift) + 1;
    5493 }
    5594
    56 if (p->prof)
    95 if (p->prof) {
    96 profdetach(p);
    5797 kfree((void *)p->prof);
    98 p->prof = 0;
    99 }
    100
    101 if (pr && pr->lo >= KERNBASE) {
    102 acquire(&kproflock);
    103 if (kprof) {
    104 // another process is profiling the kernel.
    105 release(&kproflock);
    106 kfree((void *)pr);
    107 return -1;
    108 }
    109 kprof = pr;
    110 release(&kproflock);
    111 }
    58112 p->prof = pr;
    59113 return 0;
    60114}
    61115
    @@ -64,15 +118,20 @@ sys_profile(void)
    64118uint64
    65119sys_profget(void)
    66120{
    67121 uint64 buf;
    122 int r;
    68123 struct proc *p = myproc();
    69124
    70125 argaddr(0, &buf);
    71126 if (p->prof == 0)
    72127 return -1;
    73 return copyout(p->pagetable, p->sz, buf, (char *)p->prof,
    74 sizeof(struct prof));
    128 // other harts may be adding to a kernel profile.
    129 acquire(&kproflock);
    130 r = copyout(p->pagetable, p->sz, buf, (char *)p->prof,
    131 sizeof(struct prof));
    132 release(&kproflock);
    133 return r;
    75134}
    76135
    77136// called by kexit: write the histogram to the file prof.out
    78137// in the process's current directory.
    @@ -80,8 +139,9 @@ void
    80139profdump(struct proc *p)
    81140{
    82141 struct inode *ip;
    83142
    143 profdetach(p);
    84144 begin_op();
    85145 if ((ip = create("prof.out", T_FILE, 0, 0)) != 0) {
    86146 writei(ip, 0, (uint64)p->prof, 0, sizeof(struct prof));
    87147 iunlockput(ip);

    kernel/trap.c

    @@ -3,8 +3,9 @@
    33#include "memlayout.h"
    44#include "riscv.h"
    55#include "spinlock.h"
    66#include "proc.h"
    7#include "prof.h"
    78#include "defs.h"
    89
    910struct spinlock tickslock;
    1011uint ticks;
    @@ -82,10 +83,11 @@ usertrap(void)
    8283 kexit(-1);
    8384
    8485 // give up the CPU if this is a timer interrupt.
    8586 if (which_dev == 2) {
    86 // first record where the program was.
    87 if (p->prof)
    87 // first record where the program was. (a kernel
    88 // profile is fed by kerneltrap instead.)
    89 if (p->prof && p->prof->lo < KERNBASE)
    8890 profsample(p->prof, p->trapframe->epc);
    8991 yield();
    9092 }
    9193
    @@ -156,8 +158,12 @@ kerneltrap()
    156158 r_stval());
    157159 panic("kerneltrap");
    158160 }
    159161
    162 // record where the kernel was, for a kernel profile.
    163 if (which_dev == 2)
    164 kprofsample(sepc);
    165
    160166 // give up the CPU if this is a timer interrupt.
    161167 if (which_dev == 2 && myproc() != 0)
    162168 yield();
    163169

    user/prof.c

    @@ -1,18 +1,21 @@
    11// prof: run a command under the sampling profiler, then show
    22// where its samples landed. the report goes to the standard
    33// error, so the command's own output can be redirected.
    44//
    5// prof command args...
    5// prof command args... the command's user pcs
    6// prof -k command args... the kernel's pcs, on all harts,
    7// while the command runs
    8// prof -k 0xLO 0xHI command... only kernel pcs in [LO, HI)
    69
    710#include "kernel/types.h"
    811#include "kernel/stat.h"
    912#include "kernel/fcntl.h"
    1013#include "kernel/elf.h"
    1114#include "kernel/prof.h"
    1215#include "user/user.h"
    1316
    14#define NSYM 256
    17#define NSYM 512
    1518#define NTOP 10
    1619
    1720static struct prof pr;
    1821static uint64 symaddr[NSYM]; // the program's functions, from NAME.sym
    @@ -56,8 +59,21 @@ hexdigit(char c)
    5659 return c - 'a' + 10;
    5760 return -1;
    5861}
    5962
    63// parse a hex number such as 0x80000b4a.
    64uint64
    65hex(char *s)
    66{
    67 uint64 n = 0;
    68
    69 if (s[0] == '0' && s[1] == 'x')
    70 s += 2;
    71 while (hexdigit(*s) >= 0)
    72 n = n * 16 + hexdigit(*s++);
    73 return n;
    74}
    75
    6076// read /NAME.sym, written by the Makefile: lines of
    6177// "0000000000000058 matchhere". skip sections and files.
    6278void
    6379loadsyms(char *path)
    @@ -167,20 +183,47 @@ report(char *cmd)
    167183 fprintf(2, "\n");
    168184 }
    169185}
    170186
    187// where is the symbol called name? 0 if unknown.
    188uint64
    189symbol(char *name)
    190{
    191 for (int i = 0; i < nsym; i++)
    192 if (strcmp(symname[i], name) == 0)
    193 return symaddr[i];
    194 return 0;
    195}
    196
    171197int
    172198main(int argc, char *argv[])
    173199{
    174200 uint64 lo, hi;
    175 int shift, fd, t0;
    201 int shift, fd, kernel = 0, t0;
    202 char **cmd = argv + 1;
    176203
    177 if (argc < 2) {
    178 fprintf(2, "usage: prof command args...\n");
    204 if (argc >= 3 && strcmp(argv[1], "-k") == 0) {
    205 // profile the kernel, using kernel.sym, a copy of the
    206 // kernel's symbol table that the Makefile puts in fs.img.
    207 kernel = 1;
    208 cmd = argv + 2;
    209 loadsyms("kernel");
    210 lo = symbol("_entry");
    211 hi = symbol("etext");
    212 if (argc >= 5 && cmd[0][0] == '0' && cmd[0][1] == 'x') {
    213 lo = hex(cmd[0]);
    214 hi = hex(cmd[1]);
    215 cmd += 2;
    216 }
    217 if (lo == 0 || hi <= lo) {
    218 fprintf(2, "prof: no kernel range\n");
    219 exit(1);
    220 }
    221 } else if (argc < 2) {
    222 fprintf(2, "usage: prof [-k [0xLO 0xHI]] command args...\n");
    179223 exit(1);
    180 }
    181 if (textrange(argv[1], &lo, &hi) < 0) {
    182 fprintf(2, "prof: cannot read %s\n", argv[1]);
    224 } else if (textrange(cmd[0], &lo, &hi) < 0) {
    225 fprintf(2, "prof: cannot read %s\n", cmd[0]);
    183226 exit(1);
    184227 }
    185228
    186229 // the smallest buckets that cover the range; at least 2 bytes,
    @@ -194,10 +237,10 @@ main(int argc, char *argv[])
    194237 if (profile(lo, hi, shift) < 0) {
    195238 fprintf(2, "prof: profile failed\n");
    196239 exit(1);
    197240 }
    198 exec(argv[1], argv + 1);
    199 fprintf(2, "prof: exec %s failed\n", argv[1]);
    241 exec(cmd[0], cmd);
    242 fprintf(2, "prof: exec %s failed\n", cmd[0]);
    200243 exit(1);
    201244 }
    202245 wait(0);
    203246 fprintf(2, "prof: %d ticks\n", uptime() - t0);
    @@ -207,8 +250,9 @@ main(int argc, char *argv[])
    207250 fprintf(2, "prof: no prof.out\n");
    208251 exit(1);
    209252 }
    210253 close(fd);
    211 loadsyms(argv[1]);
    212 report(argv[1]);
    254 if (!kernel)
    255 loadsyms(cmd[0]);
    256 report(kernel ? "kernel" : cmd[0]);
    213257 exit(0);
    214258}

    user/proftest.c

    @@ -5,8 +5,9 @@
    55#include "kernel/prof.h"
    66#include "user/user.h"
    77
    88#define PGSIZE 4096
    9#define KERNBASE 0x80000000L
    910
    1011static struct prof pr;
    1112static int failed;
    1213
    @@ -145,9 +146,9 @@ exitdump(void)
    145146
    146147 unlink("prof.out");
    147148 if (fork() == 0) {
    148149 profile(0, PGSIZE, 3);
    149 hot(10);
    150 hot(20);
    150151 exit(0); // no profile(0, 0, 0): kexit writes prof.out
    151152 }
    152153 wait(&xs);
    153154 memset(&pr, 0, sizeof(pr));
    @@ -180,8 +181,32 @@ leaks(void)
    180181 printf("proftest: free pages %d before, %d after\n", free0, free1);
    181182 check("no leaks", free0 == free1);
    182183}
    183184
    185void
    186kernelmode(void)
    187{
    188 int xs = 1;
    189 uint64 lo = KERNBASE, hi = KERNBASE + (PROFBUCKETS << 6);
    190
    191 if (profile(lo, hi, 6) < 0) {
    192 check("a kernel profile", 0);
    193 return;
    194 }
    195 if (fork() == 0)
    196 exit(profile(lo, hi, 6) == 0 ? 1 : 0);
    197 wait(&xs);
    198 check("only one kernel profile at a time", xs == 0);
    199
    200 pause(10); // the other harts are idle in the kernel
    201 profget(&pr);
    202 profile(0, 0, 0);
    203 printf("proftest: kernel: %d samples, %d outside [0x%lx, 0x%lx)\n",
    204 pr.nsample, pr.nout, pr.lo, pr.hi);
    205 check("kernel samples, none outside the profiled range",
    206 pr.nsample >= 10 && pr.nout == 0);
    207}
    208
    184209int
    185210main(int argc, char *argv[])
    186211{
    187212 if (argc == 2 && strcmp(argv[1], "exec") == 0) {
    @@ -195,8 +220,9 @@ main(int argc, char *argv[])
    195220 badargs();
    196221 hotloop();
    197222 forkexec();
    198223 exitdump();
    224 kernelmode();
    199225 leaks();
    200226 if (failed == 0)
    201227 printf("proftest: ALL OK\n");
    202228 else

6. Verify and measure

On the branch (ext/07-profiler, 8 commits), built with the project toolchain and run on three harts (-smp 3 -m 128M), in one boot: proftest four times, usertests -q, then proftest four more times. All eight proftest runs printed ALL OK. The first one and the one right after usertests:

$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 30 in range, 30 in hot()
proftest: most samples in the hot loop: OK
proftest: fork does not inherit the profile: OK
proftest: exec keeps the profile: OK
proftest: prof.out: 20 samples, 20 in range, 20 in hot()
proftest: exit writes prof.out: OK
proftest: only one kernel profile at a time: OK
proftest: kernel: 30 samples, 0 outside [0x80000000, 0x8000FE00)
proftest: kernel samples, none outside the profiled range: OK
proftest: free pages 32470 before, 32470 after
proftest: no leaks: OK
proftest: ALL OK
[...]
$ usertests -q
usertests starting
[...]
ALL TESTS PASSED
$ proftest
proftest: arguments, and profile(0, 0, 0) stops: OK
proftest: hot loop: 30 samples, 30 in range, 30 in hot()
proftest: most samples in the hot loop: OK
proftest: fork does not inherit the profile: OK
proftest: exec keeps the profile: OK
proftest: prof.out: 20 samples, 20 in range, 19 in hot()
proftest: exit writes prof.out: OK
proftest: only one kernel profile at a time: OK
proftest: kernel: 30 samples, 0 outside [0x80000000, 0x8000FE00)
proftest: kernel samples, none outside the profiled range: OK
proftest: free pages 32470 before, 32470 after
proftest: no leaks: OK
proftest: ALL OK

Over the eight runs the 30-tick hot loop got 30 samples every time, all inside hot(), and the 20-tick child that leaves prof.out got 19 or 20. (A tick-driven count can always come out one short: a 10-tick child required to get at least 10 samples was seen to get 9, which is why this child spins for 20.) The idle second of the kernel gave 30 to 33 samples: three harts, ten ticks each. usertests -q passing (its usertrap() lines, trimmed here, are the expected kills of kernmem and friends) shows that processes that never profile are unaffected, and the equal free-page counts show that profiled processes leave no page behind. usertests -q also passed under prof -k (in both kernel runs of Measure), with the kernel profile active on all three harts throughout. In the same build, /prof /grep .*.*.*z /README and /prof -k /echo hi run from a subdirectory found their .sym files, which prof opens from /.

Every commit builds on its own with make kernel/kernel fs.img (only the two existing warnings about RWX segments).

User profiles of real programs. b1 is README eight times (19,528 bytes), b2 is b1 eight times (156,224 bytes), both made with cat inside xv6.

$ prof grep .*.*.*z b1 > o
prof: 72 ticks
prof: grep: 61 samples, 0 outside [0x0, 0xB71), 4-byte buckets
by function:
  48	78%	matchstar
  13	21%	matchhere
hottest buckets:
  42	68%	0x24	matchstar+0x24
  3	4%	0x4C	matchhere+0x0
  3	4%	0x50	matchhere+0x4
  3	4%	0x94	matchhere+0x48
[...]

The pattern makes grep’s recursive matcher backtrack, and the profile finds the loop: the bucket at matchstar+0x24 holds one instruction, bnez a0 at 0x26, the first one after matchstar’s call to matchhere returns, inside its do ... while. Two thirds of all samples landed there. That concentration is partly QEMU’s doing: QEMU executes guest code in translated blocks and takes pending interrupts between blocks, so samples land on block entry points such as this return address (and proftest’s 0x112, a loop head). A test confirms it: with a loop whose body is about a hundred instructions with no branch inside, over three runs, 248 of 249 samples landed in the loop-head bucket, where an interrupt that could be taken anywhere would have spread them over the body. It also shifts time between functions: the time of a block of matchhere that ends in ret is charged to matchstar’s return address. On real hardware the samples would spread over the instructions actually executing, and the split between matchstar and matchhere could differ (reasoned, not measured). 61 samples in 72 ticks: grep was in user mode for most of its run.

$ prof wc b2 b2 b2 b2 b2 b2 > o
prof: 55 ticks
prof: wc: 1 samples, 0 outside [0x0, 0xA81), 4-byte buckets
[...]
$ prof -k wc b2 b2 b2 b2 b2 b2 > o
prof: 53 ticks
prof: kernel: 156 samples, 0 outside [0x80000000, 0x80007000), 32-byte buckets
by function:
  151	96%	scheduler
  4	2%	pop_off
  1	0%	usertrap
[...]

wc on 937 KB took 5.5 seconds and got one user sample. The kernel profile of the same run explains it: 156 of the 159 hart-ticks (3 harts × 53) were kernel samples, and 151 of them were idle schedulers. wc spends its time asleep, waiting for the disk (the buffer cache holds 30 blocks, b2 is 153). A sampling profiler measures where the CPU goes, and an I/O-bound program barely uses one.

Statistics. Six runs of prof grep .*.*.*z README (the three that printed a tick count took 9 to 12 ticks) gave 7 to 10 samples each; matchstar’s share came out as 87%, 100%, 50%, 70%, 71% and 62%. Two runs on b1 (60 and 61 samples) gave 65% and 78%. That is the spread sqrt(p(1-p)/n) predicts (16 points at n = 8, 6 at n = 60). The hot loop in proftest got 28 to 30 samples for 30 ticks in every run on the branch, including one with three CPU-bound greps competing for the three harts.

The kernel profile of usertests -q, all of the kernel’s text, 32-byte buckets:

$ prof -k usertests -q
[...]
ALL TESTS PASSED
prof: 1173 ticks
prof: kernel: 3467 samples, 0 outside [0x80000000, 0x80007000), 32-byte buckets
by function:
  3110	89%	scheduler
  265	7%	pop_off
  65	1%	usertrap
  18	0%	release
  4	0%	walk
[...]
hottest buckets:
  3110	89%	0x80001E20	scheduler+0x98
  265	7%	0x80000C40	pop_off+0x18
  65	1%	0x80002680	usertrap+0x92
[...]

The pop_off+0x18 bucket covers 0x80000c40–0x80000c5f, which contains 0x80000c50; the usertrap+0x92 bucket contains 0x80002690, the instruction after usertrap’s csrsi sstatus,2. In 1,173 ticks the three harts took about 3,519 timer interrupts (3 × 1,173, assuming one per hart per tick); 3,467 of them found the hart in the kernel, and 3,110 of those in the idle scheduler. Under QEMU, usertests mostly waits for its disk. Almost all the non-idle kernel samples are on three instructions, each the first after interrupts come back on: in pop_off (time spent in critical sections), in usertrap after intr_on() (trap entry), and in the scheduler (idle). The release bucket at 0x80000ca0 is really memset’s loop at 0x80000cbc, which shares the 32-byte bucket.

The blind spot at instruction resolution: the 642 bytes from kfree to the end of release, 2-byte buckets, over another full usertests -q run:

$ prof -k 0x80000a26 0x80000ca8 usertests -q
[...]
ALL TESTS PASSED
prof: 1351 ticks
prof: kernel: 4003 samples, 3761 outside [0x80000A26, 0x80000CA8), 2-byte buckets
by function:
  240	99%	pop_off
  1	0%	push_off
  1	0%	kalloc
hottest buckets:
  240	99%	0x80000C50	pop_off+0x28
  1	0%	0x80000B0E	kalloc+0x0
  1	0%	0x80000BAE	push_off+0x0

kalloc, kfree, acquire and release ran throughout, and no sample landed in any of their critical sections, nor in acquire’s spin loop: every interrupt that arrived there was delivered at 0x80000c50.

7. Go further