Tarantool development patches archive
 help / color / mirror / Atom feed
From: Sergey Bronnikov via Tarantool-patches <tarantool-patches@dev.tarantool.org>
To: Sergey Kaplun <skaplun@tarantool.org>,
	Sergey Bronnikov <estetus@gmail.com>
Cc: tarantool-patches@dev.tarantool.org
Subject: Re: [Tarantool-patches] [PATCH luajit] Optionally return PC position in jit.util.tracesnap().
Date: Fri, 16 Oct 2026 17:25:29 +0300	[thread overview]
Message-ID: <02dcf0b6-e145-4bf4-a9c4-37726f80ea0d@tarantool.org> (raw)
In-Reply-To: <as407pAECYT259Ci@root>

Hi, Sergey,

thanks for review! See my comments below.

Sergey

On 10/13/26 16:41, Sergey Kaplun via Tarantool-patches wrote:
> Hi, Sergey!
> Thanks for the patch!
> LGTM, after fixing my comments below.
>
> On 08.10.26, Sergey Bronnikov wrote:
>> From: Mike Pall <mike>
>>
>> Suggested by Sergey Bronnikov.
>>
>> (cherry picked from commit 8e6520a7aecd0517e792b359afbbfd7274791f5f)
>>
>> The optional `getpos` argument is needed for ljopt [1], a tool that
>> verifies the correctness of LuaJIT optimizations by translating the
>> recorded IR into SMT-LIB and checking the formulas with an SMT solver.
>> Traces often lack sufficient information for accurate modeling, so
>> ljopt extracts the address of the bytecode pointed to by the snapshot
>> directly from LuaJIT during execution.
>>
>> The LuaJIT internal API includes the function `jit.util.tracesnap(tr,
>> sn)`. In principle, this function could be extended to return the
>> program counter (pc). This patch does exactly that: an optional third
>> argument is added, and when it is truthy the second value returned by
>> `jit.util.tracesnap()` is the PC position of the snapshot in the
>> bytecode of the function where the trace starts.
>>
>> Also, the patch adds a test that records a root loop trace and
>> checks that the returned position is the `FORL` jump target (the
>> loop entry), that no position is returned without the optional
>> argument or with a false value, and that every snapshot of the
>> trace reports a valid bytecode offset.
> I don't understand this paragraph. Usually we don't describe added tests
> for the features (especially in the way "what was done" instead of "why
> it was done").

removed third paragraph and rephrased the second:

     The LuaJIT internal API includes the function
     `jit.util.tracesnap(tr, sn)`. The patch adds an optional third
     argument, and when it is truthy the second value returned by
     `jit.util.tracesnap()` is the PC position of the snapshot in the
     bytecode of the function where the trace starts.


>> 1. https://github.com/ligurio/ljopt
>>
> Please, mention the backporting issues as well.
Added
>
>> Sergey Bronnikov:
>> * added the description and the test for the feature
>> ---
>>
>> Branch: https://github.com/tarantool/luajit/tree/ligurio/gh-xxxx-tracesnap-getpos
>>
>>   src/lib_jit.c                                 |  9 +-
>>   .../lj-1472-tracesnap-getpos.test.lua         | 89 +++++++++++++++++++
>>   2 files changed, 97 insertions(+), 1 deletion(-)
>>   create mode 100644 test/tarantool-tests/lj-1472-tracesnap-getpos.test.lua
>>
>> diff --git a/src/lib_jit.c b/src/lib_jit.c
>> index 9f870f68f..5a0d38611 100644
>> --- a/src/lib_jit.c
>> +++ b/src/lib_jit.c
> <snipped>
>
>> diff --git a/test/tarantool-tests/lj-1472-tracesnap-getpos.test.lua b/test/tarantool-tests/lj-1472-tracesnap-getpos.test.lua
>> new file mode 100644
>> index 000000000..a56a7a679
>> --- /dev/null
>> +++ b/test/tarantool-tests/lj-1472-tracesnap-getpos.test.lua
>> @@ -0,0 +1,89 @@
>> +local tap = require('tap')
>> +local bit = require('bit')
>> +local jutil = require('jit.util')
>> +local vmdef = require('jit.vmdef')
>> +
>> +-- The test checks the optional `getpos` argument of
>> +-- `jit.util.tracesnap()` that makes the function return the PC
>> +-- position of the snapshot in the function bytecode.
>> +-- See also https://github.com/LuaJIT/LuaJIT/issues/1472.
>> +
>> +local test = tap.test('lj-1472-tracesnap-getpos'):skipcond({
>> +  ['Test requires JIT enabled'] = not jit.status(),
>> +})
>> +
>> +test:plan(8)
>> +
>> +-- Entries in `vmdef.bcnames` are 6 characters long.
>> +local BC_NAME_LENGTH = 6
>> +
>> +-- Return the name of the bytecode operation for the given
>> +-- instruction, that is encoded in the low byte.
>> +local function opname(ins)
>> +  local oidx = BC_NAME_LENGTH * bit.band(ins, 0xff)
>> +  local name = vmdef.bcnames:sub(oidx + 1, oidx + BC_NAME_LENGTH)
>> +  return (name:gsub('%s+$', ''))
> Minor: I suggest the more readable:
> | return name:match('%a+')
> Feel free to ignore.

this will break testcase:

tarantool> a = "FORL  "
---
...

tarantool> a:gsub('%a+', '')
---
- '  '
- 1
...

tarantool> a:gsub('%s+', '')
---
- FORL
- 1
...



>
>> +end
>> +
>> +local function payload()
>> +  local sum = 0
>> +  for i = 1, 100 do
> Minor: Typo: s/100/4/

Fixed:

  end

  local function payload()
    local sum = 0
-  for i = 1, 100 do
+  for i = 1, 4 do
      sum = sum + i
    end
    return sum

>
>> +    sum = sum + i
>> +  end
>> +  return sum
>> +end
>> +
>> +-- Find the loop back-edge (FORL) in the function bytecode and
>> +-- compute its jump target, that is the first instruction of the
> Typo: s/that/which/

Fixed:


  -- Find the loop back-edge (FORL) in the function bytecode and
--- compute its jump target, that is the first instruction of the
+-- compute its jump target, which is the first instruction of the
  -- loop body. `pc == 0` is the JCproto header, so real
  -- instructions start from `pc == 1`. JIT compilation is disabled
  -- while scanning, so this loop is not recorded as a trace.

>> +-- loop body. `pc == 0` is the JCproto header, so real
>> +-- instructions start from `pc == 1`. JIT compilation is disabled
>> +-- while scanning, so this loop is not recorded as a trace.
>> +jit.off()
>> +local nbc = jutil.funcinfo(payload).bytecodes
>> +local loop_entry
>> +for pc = 1, nbc - 1 do
>> +  local ins = jutil.funcbc(payload, pc)
>> +  if opname(ins) == 'FORL' then
>> +    -- The jump offset is relative to the next instruction and is
>> +    -- biased by `BCBIAS_J`.
>> +    local jmp = bit.rshift(ins, 16) - 0x8000
> Let's name the magic number:
> | local BCBIAS_J = 0x8000

Fixed:

  jit.off()
  local nbc = jutil.funcinfo(payload).bytecodes
  local loop_entry
+local BCBIAS_J = 0x8000
  for pc = 1, nbc - 1 do
    local ins = jutil.funcbc(payload, pc)
    if opname(ins) == 'FORL' then
      -- The jump offset is relative to the next instruction and is
      -- biased by `BCBIAS_J`.
-    local jmp = bit.rshift(ins, 16) - 0x8000
+    local jmp = bit.rshift(ins, 16) - BCBIAS_J
      loop_entry = pc + 1 + jmp
      break
    end

>
>> +    loop_entry = pc + 1 + jmp
>> +    break
>> +  end
>> +end
>> +test:isnt(loop_entry, nil, 'FORL instruction is found')
> It should be an assert instead, since this is the test precondition.
>
>> +
>> +-- Record a root loop trace in `payload`. Snapshot #0 points to
>> +-- the loop entry (the FORL jump target).
>> +jit.on()
>> +jit.flush()
>> +jit.opt.start('hotloop=1')
>> +payload()
>> +
>> +local info = jutil.traceinfo(1)
>> +test:ok(info, 'the root loop trace is recorded')
> It should be an assert instead, since this is the test precondition.

Updated:

      loop_entry = pc + 1 + jmp
      break
    end
  end
-test:isnt(loop_entry, nil, 'FORL instruction is found')
+assert(loop_entry, 'FORL instruction is found')

  -- Record a root loop trace in `payload`. Snapshot #0 points to
  -- the loop entry (the FORL jump target).

>
>> +local snap, pos = jutil.tracesnap(1, 0, true)
>> +test:istable(snap, 'the snapshot table is returned')
>> +test:is(pos, loop_entry, 'the PC position points to the loop entry')
>> +
>> +local _, pos_without = jutil.tracesnap(1, 0)
>> +test:is(pos_without, nil, 'no PC position without getpos')
>> +
>> +local _, pos_false = jutil.tracesnap(1, 0, false)
>> +test:is(pos_false, nil, 'no PC position with getpos=false')
>> +
>> +local all_valid = true
>> +for sn = 0, info.nexit - 1 do
>> +  local _, p = jutil.tracesnap(1, sn, true)
>> +  if type(p) ~= 'number' or p < 0 or p >= nbc or
>> +     jutil.funcbc(payload, p) == nil then
>> +    all_valid = false
>> +  end
>> +end
>> +test:ok(all_valid, 'positions of all snapshots are valid offsets')
>> +
>> +test:is(select('#', jutil.tracesnap(1, info.nexit, true)), 0,
>> +        'out-of-range snapshot returns nothing')
>> +
> It would be nice to add any correctness test.
> The most trivial in mind is the following:
>
> | src/luajit -Ohotloop=1 -e '
> |   local jutil = require"jit.util"
> |   local function trivial()
> |     local a = 0
> |     return a
> |   end
> |   trivial()
> |   trivial()
> |
> |   local _, b = jutil.tracesnap(1, 0, true)
> |   print(b)
> |   local _, b = jutil.tracesnap(1, 1, true)
> |   print(b)
> | '
> | 1
> | 2
>
> The snapshot is emitted at the function entry and before returning the
> result which makes it trivial to detect:
>
> | ---- TRACE 1 start (command line):3
> | 0001  KSHORT   0   0
> | 0002  RET1     0   2
> | ---- TRACE 1 IR
> | ....        SNAP   #0   [ ---- ]
> | ....        SNAP   #1   [ ---- +0   ]
>
> Don't forget to reset the hotcounters during the recording of this
> example.

ok, thanks, added:

  })

-test:plan(8)
+test:plan(10)

  -- Entries in `vmdef.bcnames` are 6 characters long.
  local BC_NAME_LENGTH = 6
@@ -87,4 +87,21 @@ test:ok(all_valid, 'positions of all snapshots are 
valid offsets')
  test:is(select('#', jutil.tracesnap(1, info.nexit, true)), 0,
          'out-of-range snapshot returns nothing')

+-- The same correctness check for a trace without a loop: the
+-- first and the last snapshots of the trivial function point to
+-- its first and second bytecode instructions, respectively.
+jit.flush()
+local function trivial()
+  local a = 0
+  return a
+end
+trivial()
+trivial()
+
+local _, pos_first = jutil.tracesnap(1, 0, true)
+test:is(pos_first, 1, 'the first snapshot points to the first instruction')
+
+local _, pos_last = jutil.tracesnap(1, 1, true)
+test:is(pos_last, 2, 'the second snapshot points to the second 
instruction')
+
  test:done(true)

>
>> +test:done(true)
>> -- 
>> 2.51.0
>>

      reply	other threads:[~2026-10-16 14:25 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-08 11:44 Sergey Bronnikov via Tarantool-patches
2026-10-13 13:41 ` Sergey Kaplun via Tarantool-patches
2026-10-16 14:25   ` Sergey Bronnikov via Tarantool-patches [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=02dcf0b6-e145-4bf4-a9c4-37726f80ea0d@tarantool.org \
    --to=tarantool-patches@dev.tarantool.org \
    --cc=estetus@gmail.com \
    --cc=sergeyb@tarantool.org \
    --cc=skaplun@tarantool.org \
    --subject='Re: [Tarantool-patches] [PATCH luajit] Optionally return PC position in jit.util.tracesnap().' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox