Skip to content

replay: Show callsite location with --srcline#2047

Merged
namhyung merged 6 commits into
namhyung:masterfrom
junyeong0619:callsite-trigger
May 25, 2026
Merged

replay: Show callsite location with --srcline#2047
namhyung merged 6 commits into
namhyung:masterfrom
junyeong0619:callsite-trigger

Conversation

@junyeong0619

Copy link
Copy Markdown
Contributor

closes #2027

I've added the callsite trigger action proposed in the issue.
Setting -T <func>@callsite records the return address of the matched
call, and uftrace replay --srcline shows it after the existing source
line, e.g. a() { /* file.c:11 from file.c:33 */.

Changes:

  • Added TRIGGER_FL_CALLSITE / EVENT_ID_CALLSITE and the callsite
    trigger action parser.
  • libmcount emits the event (8-byte return address payload) right
    before the matching ENTRY when the trigger fires.
  • Added task_find_exact_loc_addr() that uses libdw's
    dwarf_addrdie() + dwarf_getsrc_die() for PC-precise line lookup
    (the existing task_find_loc_addr() only knows the symbol's entry
    line). load_debug_info() now keeps libdw open at replay time so
    this lookup works there.
  • replay intercepts the CALLSITE event, stashes the address on the
    task, and appends from file:line to the source comment on the
    next ENTRY.
  • Added t305 covering both the live and --srcline paths.
  • Documented the new action in doc/uftrace-record.md with an example
    and a short note about line precision under optimization.

Testing on Rocky Linux 9.7 (GCC 11.5, x86_64):

DURATION     TID      FUNCTION [SOURCE]
   0.510 us [3615820] | __monstartup();
   0.187 us [3615820] | __cxa_atexit();
            [3615820] | main() { /* s-abc.c:26 */
            [3615820] |   a() { /* s-abc.c:11 from s-abc.c:33 */
            [3615820] |     b() { /* s-abc.c:16 from s-abc.c:13 */
            [3615820] |       c() { /* s-abc.c:21 from s-abc.c:18 */
   0.250 us [3615820] |         getpid();
   1.449 us [3615820] |       } /* c */
   1.744 us [3615820] |     } /* b */
   2.317 us [3615820] |   } /* a */
   2.604 us [3615820] | } /* main */

One thing I'd like your feedback on:
save_callsite_event() writes the 8-byte payload with a direct
*(uint64_t *)event->data = ... rather than the existing
mcount_memcpy4(). Using the memcpy helper here recorded zero on my
setup. My guess is strict aliasing — mcount_memcpy4 reinterprets the
source as unsigned int *, and the 8-byte return address write through
that path gets optimized away. The direct write goes through the
uint8_t event->data[] member, which is well-defined. I'm happy to add
an mcount_memcpy8 helper (or update mcount_memcpy4 to use byte-wise
loads) if you prefer either of those.

Also a precision note (also in the doc): the resolved call site line
depends on the dwarf line program. -O0 / -Og builds report the
exact line; with -O1 or higher the call instruction can be folded
with surrounding code so the reported line may shift by 1–2 lines.
-finstrument-functions builds are stable across optimization levels.
t305 forces -O0 in cflags for this reason.

Please let me know if you'd like changes to the approach or naming.
Thanks!

@namhyung

Copy link
Copy Markdown
Owner

Thanks for working on this! A few comments before looking at the code.

I feel like requiring --srcline looks like unnecessary. It'd be nice if it can always show callsite info once recorded. Maybe we can add --no-callsite option to disable it at replay time. Then the output becomes compact and can be used with --srcline if needed.

For memcpy, there's mcount_memcpy1() for byte-wise copy. I'm not sure if it's a strict alias issue but unaligned access on ARM may cause problems.

Regarding the precision of the location, I think it's out of scope. We can just show whatever compiler generated for the code and don't care about the value itself (unless it's definitely wrong). Please don't change the test code or settings just to pass the test, it'd be better to leave it failing so that we can be aware of the issue - or we can check whether it generates any callsite info.

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

  1. Decouple callsite from --srcline so that running uftrace record -T 'a@callsite' followed by a plain uftrace replay shows the callsite info on its own, as an independent trigger output.

  2. Switch the payload write to mcount_memcpy1().

  3. Keep the cflags untouched and adjust the test instead.

Will push the updates along these lines.

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Pushed the revisions. Now we can use for 4 versions.

Usage:

$ uftrace record -T 'a@callsite' tests/t-abc 

$ uftrace replay
# DURATION     TID      FUNCTION
   0.714 us [ 189072] | __monstartup();
  12.921 us [ 189072] | __cxa_atexit();
            [ 189072] | main() {
            [ 189072] |   a() { /* from s-abc.c:33 */
            [ 189072] |     b() {
            [ 189072] |       c() {
   0.390 us [ 189072] |         getpid();
   0.989 us [ 189072] |       } /* c */
   1.357 us [ 189072] |     } /* b */
   1.905 us [ 189072] |   } /* a */
   2.186 us [ 189072] | } /* main */

$ uftrace replay --srcline
# DURATION     TID      FUNCTION [SOURCE]
   0.714 us [ 189072] | __monstartup();
  12.921 us [ 189072] | __cxa_atexit();
            [ 189072] | main() { /* s-abc.c:26 */
            [ 189072] |   a() { /* s-abc.c:11 from s-abc.c:33 */
            [ 189072] |     b() { /* s-abc.c:16 */
            [ 189072] |       c() { /* s-abc.c:21 */
   0.390 us [ 189072] |         getpid();
   0.989 us [ 189072] |       } /* c */
   1.357 us [ 189072] |     } /* b */
   1.905 us [ 189072] |   } /* a */
   2.186 us [ 189072] | } /* main */

$ uftrace replay --no-callsite
# DURATION     TID      FUNCTION
   0.714 us [ 189072] | __monstartup();
  12.921 us [ 189072] | __cxa_atexit();
            [ 189072] | main() {
            [ 189072] |   a() {
            [ 189072] |     b() {
            [ 189072] |       c() {
   0.390 us [ 189072] |         getpid();
   0.989 us [ 189072] |       } /* c */
   1.357 us [ 189072] |     } /* b */
   1.905 us [ 189072] |   } /* a */
   2.186 us [ 189072] | } /* main */

$ uftrace replay --srcline --no-callsite
# DURATION     TID      FUNCTION [SOURCE]
   0.714 us [ 189072] | __monstartup();
  12.921 us [ 189072] | __cxa_atexit();
            [ 189072] | main() { /* s-abc.c:26 */
            [ 189072] |   a() { /* s-abc.c:11 */
            [ 189072] |     b() { /* s-abc.c:16 */
            [ 189072] |       c() { /* s-abc.c:21 */
   0.390 us [ 189072] |         getpid();
   0.989 us [ 189072] |       } /* c */
   1.357 us [ 189072] |     } /* b */
   1.905 us [ 189072] |   } /* a */
   2.186 us [ 189072] | } /* main */

Also Treated the write with mcount_memcpy1().

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Rewrite the commit message for the changes.

@namhyung namhyung left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I have some concerns if it would

  • work well with other events and arguments. (I'm a bit worried about the timestamp management now)
  • print "function+offset" when DWARF is not available.

Also please pick a single prefix in the commit subject line. More general and/or important one would be preferred. Maybe you want to split the commit otherwise.

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Here is full dump and replay output.

root@server:~/uftrace# uftrace record -W var:mydata -T 'bar@callsite' -T 'foo@callsite' t-watch
root@server:~/uftrace# uftrace dump | head -50
uftrace file header: magic         = 4674726163652100
uftrace file header: version       = 4
uftrace file header: header size   = 40
uftrace file header: endian        = 1 (little)
uftrace file header: class         = 2 (64 bit)
uftrace file header: features      = 0x1763 (PLTHOOK | TASK_SESSION | SYM_REL_ADDR | MAX_STACK | PERF_EVENT | AUTO_ARGS | DEBUG_INFO | SYM_SIZE)
uftrace file header: info          = 0xfbff (EXE_NAME | EXE_BUILD_ID | EXIT_STATUS | CMDLINE | CPUINFO | MEMINFO | OSINFO | TASKINFO | USAGEINFO | LOADINFO | RECORD_DATE | PATTERN_TYPE | VERSION | UTC_OFFSET | SYSCTL)

reading 1355229.dat
646618.687034679  1355229: [entry] __monstartup(62efea147090) depth: 0
646618.687036364  1355229: [exit ] __monstartup(62efea147090) depth: 0
646618.687037419  1355229: [entry] __cxa_atexit(62efea1470a0) depth: 0
646618.687037792  1355229: [exit ] __cxa_atexit(62efea1470a0) depth: 0
646618.687038474  1355229: [entry] main(62efea147269) depth: 0
646618.687038640  1355229: [event] trigger:callsite(186ad) depth: 0
646618.687038640  1355229: [data ] length = 8
  trigger:callsite: callsite=0x62efea14729d
646618.687038641  1355229: [entry] foo(62efea147233) depth: 1
646618.687038895  1355229: [event] trigger:callsite(186ad) depth: 0
646618.687038895  1355229: [data ] length = 8
  trigger:callsite: callsite=0x62efea14724a
646618.687038895  1355229: [event] watch:var(186ac) depth: 0
646618.687038895  1355229: [data ] length = 12
  watch:var: mydata=1
646618.687038896  1355229: [entry] bar(62efea14720b) depth: 2
646618.687040199  1355229: [exit ] bar(62efea14720b) depth: 2
646618.687041376  1355229: [event] watch:var(186ac) depth: 0
646618.687041376  1355229: [data ] length = 12
  watch:var: mydata=2
646618.687041377  1355229: [exit ] foo(62efea147233) depth: 1
646618.687041612  1355229: [exit ] main(62efea147269) depth: 0
reading perf-cpu4.dat
646618.675870741  1355229: [event] linux:task-name(200006)
646618.685026396  1355229: [event] linux:sched-out (pre-empted)(200007)
646618.685077278  1355229: [event] linux:sched-in(200001)
646618.687172426  1355229: [event] linux:task-exit(200005)

root@server:~/uftrace# uftrace replay
# DURATION     TID      FUNCTION
   1.685 us [1355229] | __monstartup();
   0.373 us [1355229] | __cxa_atexit();
            [1355229] | main() {
            [1355229] |   foo() { /* from tests/s-watch-global.c:26 */
            [1355229] |     /* watch:var (mydata=1) */
   1.303 us [1355229] |     bar(); /* from tests/s-watch-global.c:15 */
            [1355229] |     /* watch:var (mydata=2) */
   2.736 us [1355229] |   } /* foo */
   3.138 us [1355229] | } /* main */

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Followed up with a deliberate-collision case using watch:var on a global variable changed inside the traced functions

(tests/s-watch-global.c):

  $ uftrace record -W var:mydata -T 'foo@callsite' -T 'bar@callsite' t-watch
  $ uftrace dump | head -40

  646618.687038895  [event] trigger:callsite        # entry-1 for bar
  646618.687038895  [event] watch:var (mydata=1)    # same nanosecond
  646618.687038896  [entry] bar

Two events do share a timestamp here. The ordering is still deterministic: mtdp->event[] is push-ordered, and
mcount_entry_filter_record() calls save_callsite_event() before save_watchpoint(), so the callsite is written to the trace first and replay processes them in the same order.

The resulting replay output keeps both visible without any mixing:

  $ uftrace replay
              [1355229] | main() {
              [1355229] |   foo() { /* from tests/s-watch-global.c:26 /
              [1355229] |     / watch:var (mydata=1) /
     1.303 us [1355229] |     bar(); / from tests/s-watch-global.c:15 /
              [1355229] |     / watch:var (mydata=2) /
     2.736 us [1355229] |   } / foo /
     3.138 us [1355229] | } / main */

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Also treated the case that Dwarf is not available.

root@server:~/uftrace# uftrace record -T 'a@callsite' tests/t-abc-nodbg (t-abc with no -g)
root@server:~/uftrace# uftrace replay
# DURATION     TID      FUNCTION
   0.632 us [1360511] | __monstartup();
   0.282 us [1360511] | __cxa_atexit();
            [1360511] | main() {
            [1360511] |   a() { /* from main+0x41 */
            [1360511] |     b() {
            [1360511] |       c() {
   0.265 us [1360511] |         getpid();
   0.801 us [1360511] |       } /* c */
   1.066 us [1360511] |     } /* b */
   1.477 us [1360511] |   } /* a */
   1.747 us [1360511] | } /* main */

@namhyung

Copy link
Copy Markdown
Owner

Thanks, I've check it with other options and it has some side effects.

$ uftrace -a -F main ~/tmp/progs/abc
# DURATION     TID      FUNCTION
            [  10623] | main(1, 0x7ffd623464f8) {
            [  10623] |   a() {
            [  10623] |     b() {
            [  10623] |       c() {
   0.427 us [  10623] |         getpid() = 10623;
   2.495 us [  10623] |       } = 10623; /* c */
   2.913 us [  10623] |     } = 10624; /* b */
   3.169 us [  10623] |   } = 10623; /* a */
   3.950 us [  10623] | } = 0; /* main */
$ uftrace -T a@read=pmu-cycle -F main ~/tmp/progs/abc
# DURATION     TID      FUNCTION
            [  10657] | main() {
            [  10657] |   a() {
            [  10657] |     /* read:pmu-cycle (cycles=604 instructions=48) */
            [  10657] |     b() {
            [  10657] |       c() {
   0.582 us [  10657] |         getpid();
   1.334 us [  10657] |       } /* c */
   1.579 us [  10657] |     } /* b */
            [  10657] |     /* diff:pmu-cycle (cycles=+5857 instructions=+4554 IPC=0.78) */
  24.601 us [  10657] |   } /* a */
  30.448 us [  10657] | } /* main */

And if they are used together, it doesn't work well.

$ uftrace -a -T a@read=pmu-cycle,callsite -F main ~/tmp/progs/abc
# DURATION     TID      FUNCTION
            [  10690] | main(1, 0x7ffd169dab78) {
            [  10690] |   a() { /* from tests/s-abc.c:33 */
            [  10690] |     /* read:pmu-cycle (cycles=591 instructions=48) */
            [  10690] |     b() {
            [  10690] |       c() {
   0.446 us [  10690] |         getpid() = 10690;
   2.304 us [  10690] |       } = 10690; /* c */
   2.678 us [  10690] |     } = 10691; /* b */
  23.815 us [  10690] |   } = 10690; /* a */
  24.946 us [  10690] | } = 0; /* main */

So it lost the diff:pmu-cycle event. Removing the argument (-a) gives me the correct result.

$ uftrace -T a@read=pmu-cycle,callsite -F main ~/tmp/progs/abc
# DURATION     TID      FUNCTION
            [  10710] | main() {
            [  10710] |   a() { /* from tests/s-abc.c:33 */
            [  10710] |     /* read:pmu-cycle (cycles=582 instructions=48) */
            [  10710] |     b() {
            [  10710] |       c() {
   0.468 us [  10710] |         getpid();
   1.228 us [  10710] |       } /* c */
   1.496 us [  10710] |     } /* b */
            [  10710] |     /* diff:pmu-cycle (cycles=+6834 instructions=+4784 IPC=0.70) */
  25.378 us [  10710] |   } /* a */
  30.669 us [  10710] | } /* main */

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Thanks for review and testing! The lost diff event turned out to be a pre-existing bug from combining -a/-A/-R with @READ, not from the callsite trigger. I sent a separate PR with the fix: #2048.

@namhyung namhyung left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the update!

Comment thread utils/event.c Outdated
xasprintf(&evt_name, "watch:var");
break;
case EVENT_ID_CALLSITE:
xasprintf(&evt_name, "trigger:callsite");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Let's show it as just "callsite".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed it to callsite.

Comment thread utils/data-file.c Outdated
* @callsite trigger), unless the user opted out
*/
needs_dbg = opts->srcline || (opts->loc_filter != NULL);
if (!opts->no_callsite && (handle->hdr.feat_mask & DEBUG_INFO))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The location filter check is new. Also I think checking DEBUG_INFO is not enough and you need to add a new CALLSITE feature bit. Loading dwarf file should depend on the CALLSITE flag.

Also opts->no_callsite doesn't exist at this point which means it'd break the build and bisect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a CALLSITE feature bit (dwarf auto-load now depends on it), reverted the accidental loc_filter check in read_task_file(), and verified the entire series bisects cleanly.

Comment thread utils/dwarf.c Outdated
* match the trailing path against existing entries so callsite filenames
* stay consistent with the --srcline output
*/
static struct uftrace_dbg_file *find_or_get_debug_file(struct uftrace_dbg_info *dinfo,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Maybe rename to get_debug_file_suffix()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed it to get_debug_file_suffix.

Comment thread utils/session.c Outdated

/*
* PC-precise variant of task_find_loc_addr() — returns the source line
* at the instruction, not the enclosing function's entry

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

It returns a boolean. Also you need to mention that it uses DWARF directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the comment to mention DWARF usage and the bool return.

Comment thread cmds/replay.c
if (task_find_exact_loc_addr(sessions, task, rstack->time,
task->callsite_ip, &csloc)) {
snprintf(cs_buf, sizeof(cs_buf), "%s:%d", csloc.file->name,
csloc.line);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

You need to make sure it has NUL character at the end.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added explicit cs_buf[sizeof(cs_buf) - 1] = '\0' after the snprintf (and same in the function+offset helper).

Comment thread cmds/replay.c Outdated
snprintf(cs_buf, sizeof(cs_buf), "%s+%#lx",
cs_sym->name,
(unsigned long)(mod_addr -
cs_sym->addr));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Doesn't look pretty. Can we reduce the indention level or factor it out as a function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted into a format_callsite_symbol() helper.

Comment thread uftrace.h Outdated
bool list_event;
bool event_skip_out;
bool no_event;
bool no_callsite;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Personally, I'd like to keep the options as positive even if the option name itself is negative. For example, I think it's natural to check if (opts->callsite) rather than if (!opts->no_callsite).

I don't plan to change the existing ones, but I want new ones in positive form.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Treated it as positive.

Comment thread tests/t305_replay_callsite.py Outdated
return TestBase.build(self, name, cflags, ldflags)

def setup(self):
self.option = '-T a@callsite -T b@callsite -T c@callsite'

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Couldn't it be -T '^.$@callsite'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Tests still pass.

Comment thread tests/t305_replay_callsite.py Outdated
# strip directory prefix from filenames in comments
rest = re.sub(r'/[\w./-]+/(\S+\.c)', r'\1', rest)
# normalize source line numbers
rest = re.sub(r'(\.c):\d+', r'\1:N', rest)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Maybe you can just combine two lines as re.sub(r'/[\w./-]+/(\S+\.c):\d+', r'\1:N', rest).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@namhyung namhyung left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the update, I think it's almost there. Please make sure to rebase your changes on to the latest commit whenever you push them.

Comment thread doc/uftrace-record.md
The call site display can be suppressed with `--no-callsite` at replay
time. The reported line itself comes from the compiler's dwarf line
program and is shown as-is.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Please add this to uftrace live man page too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread utils/data-file.c Outdated
read_task_txt_file(sessions, opts->dirname, opts->with_syms ?: opts->dirname,
true, sym_rel,
opts->srcline | (opts->loc_filter != NULL)) < 0) {
opts->srcline | (opts->loc_filter != NULL) | callsite_dbg) <

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think it's better to split callsite and srcline/loc-filter as the latter only requires the dbg file. You may introduce a local variable for srcline/loc-filter like needs_srcline. That means you need to pass it to load_debug_info() through create_session().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For this to reach load_debug_info(), create_session() would need to take both flags:

create_session(sess, &smsg, dirname, symdir, exename, sym_rel_addr,
                needs_symtab, needs_srcline, needs_callsite_dbg);

Does that look right?

Comment thread utils/dwarf.c
* program at replay time. finish_debug_info() releases it.
*/
if (needs_srcline && dinfo->dw == NULL)
setup_dwarf_info(map->libname, dinfo, map->start, true);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think this part should belong to the next commit. Also I don't think it's using callsite info precisely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To make this precise, would you prefer introducing a separate flag like needs_callsite_dbg?

For example:

if (needs_callsite_dbg && dinfo->dw == NULL)
     setup_dwarf_info(map->libname, dinfo, map->start, true);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Yes, something like needs_callsite.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the quick review. I will change the check line in that way.

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Rebased onto master. If there is something wrong let me know. Thanks.

Comment thread cmds/info.c Outdated
0 &&
read_task_file(&link, fha->opts->dirname, false, false, false) < 0)
if (read_task_txt_file(&link, fha->opts->dirname, fha->opts->dirname, false, false, false,
false) < 0 &&

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Looks like a whitespace error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Whitespace is up to clang-format, so I shortened the line by introducing
char *dir = fha->opts->dirname;. Under 100 columns, clang-format leaves it alone.

Comment thread utils/data-file.c Outdated
read_task_txt_file(sessions, opts->dirname, opts->with_syms ?: opts->dirname,
true, sym_rel,
opts->srcline | (opts->loc_filter != NULL)) < 0) {
true, sym_rel, opts->srcline | (opts->loc_filter != NULL),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Similarly, let's introduce needs_srcline variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — introduced needs_srcline = opts->srcline | (opts->loc_filter != NULL).

Comment thread utils/dwarf.c Outdated
if (!debug_info_has_location(dinfo) && !debug_info_has_argspec(dinfo)) {
load_debug_file(dinfo, stab, sinfo->symdir, map->libname, map->build_id,
needs_srcline);
needs_srcline || needs_callsite);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ditto.

Comment thread utils/dwarf.c Outdated
if (!debug_info_has_location(dinfo) && !debug_info_has_argspec(dinfo)) {
load_debug_file(dinfo, &mod->symtab, dirname, mod->name, mod->build_id,
needs_srcline);
needs_srcline || needs_callsite);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't think this is necessary as callsite doesn't use the debug (.dbg) file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, callsite doesn't read the .dbg file. Dropped needs_callsite from load_debug_file() in both load_debug_info() and load_module_debug_info().

Add the `callsite` trigger action which makes uftrace remember the
return address of a matched function call.  At record time it just sets
a flag (TRIGGER_FL_CALLSITE); the actual address is captured later by
libmcount.  A new event id (EVENT_ID_CALLSITE) is reserved to carry the
return address inside the trace data.

This commit only wires up the option plumbing; the recording side and
the replay-side display are added in subsequent commits.

Signed-off-by: Jun Yeong Kim <junyeonggim5@gmail.com>
When the @callsite trigger fires at function entry, libmcount emits an
EVENT_ID_CALLSITE record whose payload is the caller's return address
(rstack->parent_ip).  The event is timestamped one nanosecond before
the entry record so it always precedes the matching ENTRY in the
trace.

The @callsite trigger also implicitly turns on debug info saving at
record time and a new CALLSITE feature bit is set on the file header
so the replay side can later resolve the recorded address to a source
line without requiring --srcline.

The replay-side reader decodes the 8-byte payload into task->args so
existing event consumers (dump, replay, ...) can find it.  event.c
gains "callsite" naming and "callsite=0xADDR" formatting for the raw
dump output.

Signed-off-by: Jun Yeong Kim <junyeonggim5@gmail.com>
task_find_loc_addr() returns the symbol's entry line stored in
dinfo->locs[], which is fine for a function definition but loses the
precision needed for a call site.

Add task_find_exact_loc_addr() which goes through libdw directly:
dwarf_addrdie() + dwarf_getsrc_die() against the function's
module-relative address.  This drills into the dwarf line program so
a PC anywhere inside a function resolves to the matching source line.

To make this work at replay time, load_debug_info() now also opens the
libdw handle per module (kept until finish_debug_info()).

The shared session/map lookup is factored into task_find_addr_module()
used by both helpers.

Signed-off-by: Jun Yeong Kim <junyeonggim5@gmail.com>
When the trace contains an EVENT_ID_CALLSITE record, intercept it in
the UFTRACE_EVENT branch, stash the return address on the task, and
suppress the default event-as-comment output.

The next ENTRY consumes that address: replay resolves it to file:line
via task_find_exact_loc_addr() and appends it to the source comment.
The call site is shown by default and can be turned off with the new
--no-callsite option.  When --srcline is also given, both the callee
definition line and the call site are shown.

When dwarf info is unavailable (e.g. when the binary is built without
-g), replay falls back to a "function+offset" form so the call site is
still informative.  The fallback path is factored into a small helper
format_callsite_symbol() to keep the caller flat.

open_data_file() also loads debug info automatically when the trace
carries the CALLSITE feature bit (unless --no-callsite is given), so
the call site is resolved without having to pass --srcline.

Simple usage:

  $ uftrace record -T 'a@callsite' t-abc
  $ uftrace replay
  # DURATION     TID      FUNCTION
              [3615820] | main() {
              [3615820] |   a() { /* from s-abc.c:33 */
              [3615820] |     b() {
              [3615820] |       c() {
              ...

  $ uftrace replay --srcline
  # DURATION     TID      FUNCTION [SOURCE]
              [3615820] | main() { /* s-abc.c:26 */
              [3615820] |   a() { /* s-abc.c:11 from s-abc.c:33 */
              ...

  $ uftrace replay --no-callsite
  # DURATION     TID      FUNCTION
              [3615820] | main() {
              [3615820] |   a() {
              ...

  $ uftrace replay     # binary built without -g
              [3615820] |   a() { /* from main+0x41 */
              ...

When the call site cannot be resolved at all (e.g. coming from libc)
the suffix is silently dropped.

closes namhyung#2027

Signed-off-by: Jun Yeong Kim <junyeonggim5@gmail.com>
t305 records s-abc with -T '^.$@callsite' and verifies that the
replay output contains a "from <file>:<line>" suffix for each
function.

The expected output normalizes line numbers and strips directory
prefixes (in sort()) so the test stays stable across optimization
levels and build environments — the exact line reported by the
compiler's dwarf line program is not asserted, only the presence of
the callsite info itself.

The default --no-event option is dropped via a runcmd() override so
the EVENT_ID_CALLSITE records reach replay (same pattern as the
existing read-trigger tests).  The test is skipped when dwarf info is
unavailable.

Signed-off-by: Jun Yeong Kim <junyeonggim5@gmail.com>
Add `callsite` to the trigger action BNF in uftrace-record.md and
uftrace-live.md and describe the new behaviour with examples showing
the "from file:N" suffix in the default replay output and the combined
"file:N from file:N" output under --srcline.

uftrace-replay.md gains a new entry for --no-callsite that explains
how to suppress the call site display, and clarifies that --srcline
only controls the callee definition line.

Signed-off-by: Jun Yeong Kim <junyeonggim5@gmail.com>

@namhyung namhyung left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM! I'm curious when you sleep.. take care. :)

@junyeong0619

Copy link
Copy Markdown
Contributor Author

Haha, just happened to wake up and couldn't help picking it up :) Thanks for the kind words — and for the thorough reviews!

@namhyung namhyung merged commit 0a4f285 into namhyung:master May 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show source line numbers for function callsites.

2 participants