Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions JSTests/stress/line-start-table-positions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// The line and column of an error come from its source's line start table. This compares them with
// positions counted here, for every kind of line terminator, for line lengths on both sides of what
// one, two and three bytes of the table's length encoding hold, and for line counts around its block size.

function shouldBe(actual, expected, message) {
if (actual !== expected)
throw new Error(`${message}: expected ${expected} but got ${actual}`);
}

let seed = 12345;
function random(n) {
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
return (seed >>> 12) % n;
}

globalThis.seen = [];
globalThis.at = function (error) {
seen.push(`${error.line}:${error.column}`);
};

function positionsOf(text) {
seen.length = 0;
(0, eval)(text);
return seen.join(" ");
}

// One-based, for ascending offsets, by reading the text one character at a time.
function positionsAt(text, offsets) {
const positions = [];
let line = 1;
let lineStart = 0;
let i = 0;
for (const offset of offsets) {
for (; i < offset; i++) {
const c = text[i];
if (c === "\r" && text[i + 1] === "\n")
i++;
else if (c !== "\n" && c !== "\r" && c !== "\u2028" && c !== "\u2029")
continue;
line++;
lineStart = i + 1;
}
positions.push(`${line}:${offset - lineStart + 1}`);
}
return positions.join(" ");
}

// Where in `at(new Error("..."));` the column of the error is.
const columnInStatement = Number(positionsOf(`at(new Error(""));`).split(":")[1]) - 1;

const terminators = {
"LF": ["\n"],
"CR LF": ["\r\n"],
"CR": ["\r"],
"LF, CR LF and CR": ["\n", "\r\n", "\r"],
"all five, 16-bit": ["\n", "\r\n", "\r", "\u2028", "\u2029"],
};

const lengths = [126, 127, 128, 129, 300, 16382, 16383, 16384, 16385, 40000];

for (const [name, choices] of Object.entries(terminators)) {
const terminator = () => choices[random(choices.length)];
let text = "";
const offsets = [];
for (let i = 0; i < 150; i++) {
for (let blank = random(4); blank > 0; blank--)
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
text += terminator();
text += " ".repeat(random(12));
offsets.push(text.length + columnInStatement);
text += `at(new Error("${"x".repeat(i < lengths.length ? lengths[i] : random(60))}"));` + terminator();
}
const expected = positionsAt(text, offsets);
shouldBe(positionsOf(text), expected, name);
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
// Again: the answers are kept.
shouldBe(positionsOf(text), expected, `${name}, second time`);
}

for (const lines of [1, 2, 63, 64, 65, 127, 128, 129, 130]) {
for (const endsWithTerminator of [false, true]) {
const text = "\n".repeat(lines - 1) + `at(new Error(""));` + (endsWithTerminator ? "\n" : "");
shouldBe(positionsOf(text), `${lines}:${columnInStatement + 1}`, `${lines} lines`);
}
}
5 changes: 2 additions & 3 deletions Source/JavaScriptCore/bytecode/CodeBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2576,9 +2576,8 @@ void CodeBlock::removeExceptionHandlerForCallSite(DisposableCallSiteIndex callSi
LineColumn CodeBlock::lineColumnForBytecodeIndex(BytecodeIndex bytecodeIndex) const
{
RELEASE_ASSERT(bytecodeIndex.offset() < instructions().size());
auto entry = m_unlinkedCode->expressionInfoForBytecodeIndex(bytecodeIndex);
unsigned divotInProvider = sourceOffset() + entry.divot;
return source().provider()->documentLineColumnForOffset(divotInProvider);
SourceProvider& provider = *source().provider();
return provider.documentLineColumn(m_unlinkedCode->lineColumnInTextForBytecodeIndex(bytecodeIndex, provider, sourceOffset()));
}

ExpressionInfo::Entry CodeBlock::expressionInfoForBytecodeIndex(BytecodeIndex bytecodeIndex) const
Expand Down
22 changes: 18 additions & 4 deletions Source/JavaScriptCore/bytecode/ExpressionInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "config.h"
#include "ExpressionInfo.h"

#include "SourceProvider.h"
#include "VM.h"
#include <numeric>
#include <wtf/DataLog.h>
Expand All @@ -49,6 +50,9 @@ namespace JSC {
This is just an optimization aid to speed up reconstruction of expression info
from the EncodedInfo.

c. A LineColumnMap cache.
This is to speed up look up of LineColumn values we have looked up before.

Encoding of EncodedInfo words
=============================

Expand Down Expand Up @@ -194,13 +198,15 @@ namespace JSC {

Backing Store and Shape
=======================
The ExpressionInfo and its backing store is allocated as a contiguous slab. We first compute
the size of the slab, then allocate it, and lastly use placement new to instantiate the
ExpressionInfo.
The ExpressionInfo and its backing store (with the exception of the contents of the
LineColumnMap cache) is allocated as a contiguous slab. We first compute the size of the slab,
then allocate it, and lastly use placement new to instantiate the ExpressionInfo.

The shape of ExpressionInfo looks like this:

ExpressionInfo: [ m_numberOfChapters ]
ExpressionInfo: [ m_cachedLineColumnsLock ]
[ m_cachedLineColumns ]
[ m_numberOfChapters ]
[ m_numberOfEncodedInfo ]
[ m_numberOfEncodedInfoExtensions ]
Chapters Start: [ chapters()[0] ]
Expand Down Expand Up @@ -890,6 +896,14 @@ auto ExpressionInfo::entryForInstPC(InstPC instPC) -> Entry
return decoder.entry();
}

LineColumn ExpressionInfo::lineColumnInTextForInstPC(InstPC instPC, SourceProvider& provider, unsigned sourceOffset)
{
Locker locker { m_cachedLineColumnsLock };
return m_cachedLineColumns.ensure(instPC, [&] {
return provider.lineColumnInTextForOffset(sourceOffset + entryForInstPC(instPC).divot);
}).iterator->value;
}

template<unsigned bitCount>
void ExpressionInfo::print(PrintStream& out, FieldID fieldID, unsigned value)
{
Expand Down
14 changes: 14 additions & 0 deletions Source/JavaScriptCore/bytecode/ExpressionInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@

#pragma once

#include <JavaScriptCore/LineColumn.h>
#include <wtf/HashMap.h>
#include <wtf/HashTraits.h>
#include <wtf/IterationStatus.h>
#include <wtf/Lock.h>
#include <wtf/MallocPtr.h>
#include <wtf/PrintStream.h>
#include <wtf/StdLibExtras.h>
Expand All @@ -37,6 +39,8 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN

namespace JSC {

class SourceProvider;

// See comment at the top of ExpressionInfo.cpp on how ExpressionInfo works.

class ExpressionInfo {
Expand Down Expand Up @@ -183,6 +187,12 @@ class ExpressionInfo {

Entry NODELETE entryForInstPC(InstPC);

// The zero-based line and column of the instruction's divot in the text of its source, where this code starts at
// sourceOffset. entryForInstPC() decodes from the start of the chapter on every call, and stack traces ask for the
// same instructions again, so this keeps each answer. Sources that share unlinked code have the same text
// (CodeCache), so the answer is the same for all of them. Where a source says its text starts is not in it.
LineColumn lineColumnInTextForInstPC(InstPC, SourceProvider&, unsigned sourceOffset);

bool isEmpty() const { return !m_numberOfEncodedInfo; };
size_t NODELETE byteSize() const; // owned by this object
size_t NODELETE byteSizeForGCPacing() const; // what a generated (non-borrowed) one this size would own
Expand Down Expand Up @@ -312,7 +322,11 @@ class ExpressionInfo {

static constexpr unsigned numberOfWordsBetweenChapters = 10000;

using LineColumnMap = UncheckedKeyHashMap<InstPC, LineColumn, WTF::IntHash<InstPC>, WTF::UnsignedWithZeroKeyHashTraits<InstPC>>;

// With Options::useSourceCodeDump(), compiler threads ask too.
Lock m_cachedLineColumnsLock;
LineColumnMap m_cachedLineColumns WTF_GUARDED_BY_LOCK(m_cachedLineColumnsLock);
unsigned m_numberOfChapters;
unsigned m_numberOfEncodedInfo;
unsigned m_numberOfEncodedInfoExtensions;
Expand Down
5 changes: 5 additions & 0 deletions Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ ExpressionInfo::Entry UnlinkedCodeBlock::expressionInfoForBytecodeIndex(Bytecode
return expressionInfo().entryForInstPC(bytecodeIndex.offset());
}

LineColumn UnlinkedCodeBlock::lineColumnInTextForBytecodeIndex(BytecodeIndex bytecodeIndex, SourceProvider& provider, unsigned sourceOffset)
{
return expressionInfo().lineColumnInTextForInstPC(bytecodeIndex.offset(), provider, sourceOffset);
}

#ifndef NDEBUG
static void dumpExpressionInfoDetails(size_t index, const JSInstructionStream& instructionStream, unsigned instructionOffset, unsigned divot, unsigned startOffset, unsigned endOffset)
{
Expand Down
3 changes: 2 additions & 1 deletion Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class UnlinkedCodeBlock : public JSCell {
bool hasExpressionInfo() { return !expressionInfo().isEmpty(); }
// Null while the expression info is still in the cache payload. For a caller that cannot take m_lock or allocate
// (a sampling hook inside malloc): unlike expressionInfo() it never decodes. On the result use entryForInstPC(),
// which does neither; lineColumnForInstPC() fills a cache.
// which does neither; lineColumnInTextForInstPC() fills a cache.
ExpressionInfo* expressionInfoIfDecoded() const { return m_expressionInfo.get(); }

bool hasCheckpoints() const { return m_hasCheckpoints; }
Expand Down Expand Up @@ -291,6 +291,7 @@ class UnlinkedCodeBlock : public JSCell {
bool hasRareData() const { return m_rareData.get(); }

ExpressionInfo::Entry expressionInfoForBytecodeIndex(BytecodeIndex);
LineColumn lineColumnInTextForBytecodeIndex(BytecodeIndex, SourceProvider&, unsigned sourceOffset);

bool typeProfilerExpressionInfoForBytecodeOffset(unsigned bytecodeOffset, unsigned& startDivot, unsigned& endDivot);

Expand Down
Loading
Loading