Commit 659d106a authored by Danylo Boiko's avatar Danylo Boiko Committed by V8 LUCI CQ

[turbolizer] Phases layer initial commit

- Added separate layer for phases
- Moved json parsing logic to new layer

Change-Id: I16289b1f4f62d011c1801fbc37bac49ff911e61e
Bug: v8:7327
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3695566
Commit-Queue: Danylo Boiko <danielboyko02@gmail.com>
Reviewed-by: 's avatarNico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81081}
parent dd6d96c8
......@@ -6,6 +6,27 @@ export function anyToString(x: any): string {
return `${x}`;
}
export function snakeToCamel(str: string): string {
return str.toLowerCase().replace(/([-_][a-z])/g, group =>
group
.toUpperCase()
.replace('-', '')
.replace('_', ''));
}
export function camelize(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(value => camelize(value));
} else if (obj !== null && obj.constructor === Object) {
return Object.keys(obj).reduce((result, key) => ({
...result,
[snakeToCamel(key)]: camelize(obj[key])
}), {},
);
}
return obj;
}
export function sortUnique<T>(arr: Array<T>, f: (a: T, b: T) => number, equal: (a: T, b: T) => boolean): Array<T> {
if (arr.length == 0) return arr;
arr = arr.sort(f);
......
import { GNode, MINIMUM_EDGE_SEPARATION } from "./node";
import { Edge } from "./edge";
import { GraphPhase } from "./phases/graph-phase";
export class Graph {
nodeMap: Array<GNode>;
......@@ -12,7 +13,7 @@ export class Graph {
width: number;
height: number;
constructor(data: any) {
constructor(graphPhase: GraphPhase) {
this.nodeMap = [];
this.minGraphX = 0;
......@@ -22,13 +23,13 @@ export class Graph {
this.width = 1;
this.height = 1;
data.nodes.forEach((jsonNode: any) => {
graphPhase.data.nodes.forEach((jsonNode: GNode) => {
this.nodeMap[jsonNode.id] = new GNode(jsonNode.nodeLabel);
});
data.edges.forEach((e: any) => {
const t = this.nodeMap[e.target];
const s = this.nodeMap[e.source];
graphPhase.data.edges.forEach((e: any) => {
const t = this.nodeMap[e.target.id];
const s = this.nodeMap[e.source.id];
const newEdge = new Edge(t, e.index, s, e.type);
t.inputs.push(newEdge);
s.outputs.push(newEdge);
......@@ -37,7 +38,6 @@ export class Graph {
s.cfg = true;
}
});
}
*nodes(p = (n: GNode) => true) {
......
......@@ -7,8 +7,9 @@ import { ScheduleView } from "./views/schedule-view";
import { SequenceView } from "./views/sequence-view";
import { SourceResolver } from "./source-resolver";
import { SelectionBroker } from "./selection/selection-broker";
import { View, PhaseView } from "./views/view";
import { PhaseView, View } from "./views/view";
import { GNode } from "./node";
import { GraphPhase } from "./phases/graph-phase";
const multiviewID = "multiview";
......@@ -83,7 +84,7 @@ export class GraphMultiView extends View {
view.sourceResolver.forEachPhase(phase => {
const optionElement = document.createElement("option");
let maxNodeId = "";
if (phase.type == "graph" && phase.highestNodeId != 0) {
if (phase instanceof GraphPhase && phase.highestNodeId != 0) {
maxNodeId = ` ${phase.highestNodeId}`;
}
optionElement.text = `${phase.name}${maxNodeId}`;
......
......@@ -176,6 +176,8 @@ export class GNode {
((this.nodeLabel.opcode == "Phi" || this.nodeLabel.opcode == "EffectPhi" || this.nodeLabel.opcode == "InductionVariablePhi") &&
this.inputs[this.inputs.length - 1].source.nodeLabel.opcode == "Loop");
}
public identifier = (): string => `${this.id}`;
}
export const nodeToStr = (n: GNode) => "N" + n.id;
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export abstract class Origin {
phase: string;
reducer: string;
constructor(phase: string, reducer: string) {
this.phase = phase;
this.reducer = reducer;
}
}
export class NodeOrigin extends Origin {
nodeId: number;
constructor(nodeId: number, phase: string, reducer: string) {
super(phase, reducer);
this.nodeId = nodeId;
}
public identifier = (): string => `${this.nodeId}`;
public toString = (): string => `#${this.nodeId} in phase '${this.phase}/${this.reducer}'`;
}
export class BytecodeOrigin extends Origin {
bytecodePosition: number;
constructor(bytecodePosition: number, phase: string, reducer: string) {
super(phase, reducer);
this.bytecodePosition = bytecodePosition;
}
public identifier = (): string => `${this.bytecodePosition}`;
public toString = (): string => {
return `Bytecode line ${this.bytecodePosition} in phase '${this.phase}/${this.reducer}'`;
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "./phase";
export class DisassemblyPhase extends Phase {
data: string;
blockIdToOffset: Array<number>;
blockStartPCtoBlockIds: Map<number, Array<number>>;
constructor(name: string, data: string) {
super(name, PhaseType.Disassembly);
this.data = data;
this.blockIdToOffset = new Array<number>() ;
this.blockStartPCtoBlockIds = new Map<number, Array<number>>();
}
public parseBlockIdToOffsetFromJSON(blockIdToOffsetJson): void {
if (!blockIdToOffsetJson) return;
for (const [blockId, offset] of Object.entries<number>(blockIdToOffsetJson)) {
this.blockIdToOffset[blockId] = offset;
if (!this.blockStartPCtoBlockIds.has(offset)) {
this.blockStartPCtoBlockIds.set(offset, new Array<number>());
}
this.blockStartPCtoBlockIds.get(offset).push(Number(blockId));
}
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "./phase";
import { NodeLabel } from "../node-label";
import { GNode } from "../node";
import { Edge } from "../edge";
import { BytecodeOrigin, NodeOrigin } from "../origin";
import { SourcePosition } from "../position";
export class GraphPhase extends Phase {
data: GraphData;
highestNodeId: number;
nodeLabelMap: Array<NodeLabel>;
nodeIdToNodeMap: Array<GNode>;
constructor(name: string, highestNodeId: number, data?: GraphData,
nodeLabelMap?: Array<NodeLabel>, nodeIdToNodeMap?: Array<GNode>) {
super(name, PhaseType.Graph);
this.highestNodeId = highestNodeId;
this.data = data ?? new GraphData();
this.nodeLabelMap = nodeLabelMap ?? new Array<NodeLabel>();
this.nodeIdToNodeMap = nodeIdToNodeMap ?? new Array<GNode>();
}
public parseDataFromJSON(dataJson, nodeLabelMap: Array<NodeLabel>): void {
this.nodeIdToNodeMap = this.parseNodesFromJSON(dataJson.nodes, nodeLabelMap);
this.parseEdgesFromJSON(dataJson.edges);
}
private parseNodesFromJSON(nodesJSON, nodeLabelMap: Array<NodeLabel>): Array<GNode> {
const nodeIdToNodeMap = new Array<GNode>();
for (const node of nodesJSON) {
let origin: NodeOrigin | BytecodeOrigin = null;
const jsonOrigin = node.origin;
if (jsonOrigin) {
if (jsonOrigin.nodeId) {
origin = new NodeOrigin(jsonOrigin.nodeId, jsonOrigin.reducer, jsonOrigin.phase);
} else {
origin = new BytecodeOrigin(jsonOrigin.bytecodePosition, jsonOrigin.reducer,
jsonOrigin.phase);
}
}
let sourcePosition: SourcePosition = null;
if (node.sourcePosition) {
const scriptOffset = node.sourcePosition.scriptOffset;
const inliningId = node.sourcePosition.inliningId;
sourcePosition = new SourcePosition(scriptOffset, inliningId);
}
const label = new NodeLabel(node.id, node.label, node.title, node.live, node.properties,
sourcePosition, origin, node.opcode, node.control, node.opinfo, node.type);
const previous = nodeLabelMap[label.id];
if (!label.equals(previous)) {
if (previous !== undefined) {
label.setInplaceUpdatePhase(this.name);
}
nodeLabelMap[label.id] = label;
}
const newNode = new GNode(label);
this.data.nodes.push(newNode);
nodeIdToNodeMap[newNode.id] = newNode;
}
return nodeIdToNodeMap;
}
private parseEdgesFromJSON(edgesJSON): void {
for (const edge of edgesJSON) {
const target = this.nodeIdToNodeMap[edge.target];
const source = this.nodeIdToNodeMap[edge.source];
const newEdge = new Edge(target, edge.index, source, edge.type);
this.data.edges.push(newEdge);
target.inputs.push(newEdge);
source.outputs.push(newEdge);
if (edge.type === "control") {
source.cfg = true;
}
}
}
}
export class GraphData {
nodes: Array<GNode>;
edges: Array<Edge>;
constructor(nodes?: Array<GNode>, edges?: Array<Edge>) {
this.nodes = nodes ?? new Array<GNode>();
this.edges = edges ?? new Array<Edge>();
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "./phase";
export class InstructionsPhase extends Phase {
// Maps node ids to instruction ranges.
nodeIdToInstructionRange?: Array<[number, number]>;
// Maps block ids to instruction ranges.
blockIdToInstructionRange?: Array<[number, number]>;
instructionOffsetToPCOffset?: Array<[number, number]>;
codeOffsetsInfo?: CodeOffsetsInfo;
// Maps instruction numbers to PC offsets.
instructionToPCOffset: Array<TurbolizerInstructionStartInfo>;
// Maps PC offsets to instructions.
pcOffsetToInstructions: Map<number, Array<number>>;
pcOffsets: Array<number>;
constructor(name: string, nodeIdToInstructionRange?: Array<[number, number]>,
blockIdToInstructionRange?: Array<[number, number]>,
codeOffsetsInfo?: CodeOffsetsInfo) {
super(name, PhaseType.Instructions);
this.nodeIdToInstructionRange = nodeIdToInstructionRange ?? new Array<[number, number]>();
this.blockIdToInstructionRange = blockIdToInstructionRange ?? new Array<[number, number]>();
this.codeOffsetsInfo = codeOffsetsInfo;
this.instructionToPCOffset = new Array<TurbolizerInstructionStartInfo>();
this.pcOffsetToInstructions = new Map<number, Array<number>>();
this.pcOffsets = new Array<number>();
}
public parseNodeIdToInstructionRangeFromJSON(nodeIdToInstructionJson): void {
if (!nodeIdToInstructionJson) return;
for (const [nodeId, range] of Object.entries<[number, number]>(nodeIdToInstructionJson)) {
this.nodeIdToInstructionRange[nodeId] = range;
}
}
public parseBlockIdToInstructionRangeFromJSON(blockIdToInstructionRangeJson): void {
if (!blockIdToInstructionRangeJson) return;
for (const [blockId, range] of
Object.entries<[number, number]>(blockIdToInstructionRangeJson)) {
this.blockIdToInstructionRange[blockId] = range;
}
}
public parseInstructionOffsetToPCOffsetFromJSON(instructionOffsetToPCOffsetJson): void {
if (!instructionOffsetToPCOffsetJson) return;
for (const [instruction, numberOrInfo] of Object.entries<number |
TurbolizerInstructionStartInfo>(instructionOffsetToPCOffsetJson)) {
let info: TurbolizerInstructionStartInfo = null;
if (typeof numberOrInfo === "number") {
info = new TurbolizerInstructionStartInfo(numberOrInfo, numberOrInfo, numberOrInfo);
} else {
info = new TurbolizerInstructionStartInfo(numberOrInfo.gap, numberOrInfo.arch,
numberOrInfo.condition);
}
this.instructionToPCOffset[instruction] = info;
if (!this.pcOffsetToInstructions.has(info.gap)) {
this.pcOffsetToInstructions.set(info.gap, new Array<number>());
}
this.pcOffsetToInstructions.get(info.gap).push(Number(instruction));
}
this.pcOffsets = Array.from(this.pcOffsetToInstructions.keys()).sort((a, b) => b - a);
}
public parseCodeOffsetsInfoFromJSON(codeOffsetsInfoJson: CodeOffsetsInfo): void {
if (!codeOffsetsInfoJson) return;
this.codeOffsetsInfo = new CodeOffsetsInfo(codeOffsetsInfoJson.codeStartRegisterCheck,
codeOffsetsInfoJson.deoptCheck, codeOffsetsInfoJson.initPoison,
codeOffsetsInfoJson.blocksStart, codeOffsetsInfoJson.outOfLineCode,
codeOffsetsInfoJson.deoptimizationExits, codeOffsetsInfoJson.pools,
codeOffsetsInfoJson.jumpTables);
}
}
export class CodeOffsetsInfo {
codeStartRegisterCheck: number;
deoptCheck: number;
initPoison: number;
blocksStart: number;
outOfLineCode: number;
deoptimizationExits: number;
pools: number;
jumpTables: number;
constructor(codeStartRegisterCheck: number, deoptCheck: number, initPoison: number,
blocksStart: number, outOfLineCode: number, deoptimizationExits: number,
pools: number, jumpTables: number) {
this.codeStartRegisterCheck = codeStartRegisterCheck;
this.deoptCheck = deoptCheck;
this.initPoison = initPoison;
this.blocksStart = blocksStart;
this.outOfLineCode = outOfLineCode;
this.deoptimizationExits = deoptimizationExits;
this.pools = pools;
this.jumpTables = jumpTables;
}
}
export class TurbolizerInstructionStartInfo {
gap: number;
arch: number;
condition: number;
constructor(gap: number, arch: number, condition: number) {
this.gap = gap;
this.arch = arch;
this.condition = condition;
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export abstract class Phase {
name: string;
type: PhaseType;
constructor(name, type: PhaseType) {
this.name = name;
this.type = type;
}
}
export enum PhaseType {
Graph = "graph",
TurboshaftGraph = "turboshaft_graph",
Disassembly = "disassembly",
Instructions = "instructions",
Sequence = "sequence",
Schedule = "schedule"
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "./phase";
export class SchedulePhase extends Phase {
data: string;
schedule: { currentBlock, blocks: Array<any>, nodes: Array<any> };
constructor(name: string, data: string, schedule?: any) {
super(name, PhaseType.Schedule);
this.data = data;
this.schedule = schedule ?? {
currentBlock: undefined,
blocks: new Array<any>(),
nodes: new Array<any>()
};
}
public parseScheduleFromJSON(scheduleDataJson): void {
const lines = scheduleDataJson.split(/[\n]/);
nextLine:
for (const line of lines) {
for (const rule of this.parsingRules) {
for (const lineRegexp of rule.lineRegexps) {
const match = line.match(lineRegexp);
if (match) {
rule.process(match);
continue nextLine;
}
}
}
console.warn(`Unmatched schedule line \"${line}\"`);
}
}
private createNode = match => {
let inputs = [];
if (match.groups.args) {
const nodeIdsString = match.groups.args.replace(/\s/g, '');
const nodeIdStrings = nodeIdsString.split(',');
inputs = nodeIdStrings.map(n => Number.parseInt(n, 10));
}
const node = {
id: Number.parseInt(match.groups.id, 10),
label: match.groups.label,
inputs: inputs
};
if (match.groups.blocks) {
const nodeIdsString = match.groups.blocks.replace(/\s/g, '').replace(/B/g, '');
const nodeIdStrings = nodeIdsString.split(',');
this.schedule.currentBlock.succ = nodeIdStrings.map(n => Number.parseInt(n, 10));
}
this.schedule.nodes[node.id] = node;
this.schedule.currentBlock.nodes.push(node);
}
private createBlock = match => {
let predecessors = [];
if (match.groups.in) {
const blockIdsString = match.groups.in.replace(/\s/g, '').replace(/B/g, '');
const blockIdStrings = blockIdsString.split(',');
predecessors = blockIdStrings.map(n => Number.parseInt(n, 10));
}
const block = {
id: Number.parseInt(match.groups.id, 10),
isDeferred: match.groups.deferred != undefined,
pred: predecessors.sort(),
succ: [],
nodes: []
};
this.schedule.blocks[block.id] = block;
this.schedule.currentBlock = block;
}
private setGotoSuccessor = match => {
this.schedule.currentBlock.succ = [Number.parseInt(match.groups.successor.replace(/\s/g, ''), 10)];
}
private parsingRules = [
{
lineRegexps: [
/^\s*(?<id>\d+):\ (?<label>.*)\((?<args>.*)\)$/,
/^\s*(?<id>\d+):\ (?<label>.*)\((?<args>.*)\)\ ->\ (?<blocks>.*)$/,
/^\s*(?<id>\d+):\ (?<label>.*)$/
],
process: this.createNode
},
{
lineRegexps: [/^\s*---\s*BLOCK\ B(?<id>\d+)\s*(?<deferred>\(deferred\))?(\ <-\ )?(?<in>[^-]*)?\ ---$/],
process: this.createBlock
},
{
lineRegexps: [/^\s*Goto\s*->\s*B(?<successor>\d+)\s*$/],
process: this.setGotoSuccessor
}
];
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "./phase";
export class SequencePhase extends Phase {
blocks: Array<any>;
registerAllocation: RegisterAllocation;
constructor(name: string, blocks: Array<any>, registerAllocation?: RegisterAllocation) {
super(name, PhaseType.Sequence);
this.blocks = blocks;
this.registerAllocation = registerAllocation;
}
}
export interface RegisterAllocation {
fixedDoubleLiveRanges: Map<string, Range>;
fixedLiveRanges: Map<string, Range>;
liveRanges: Map<string, Range>;
}
export interface Range {
childRanges: Array<ChildRange>;
isDeferred: boolean;
}
export interface ChildRange {
id: string;
type: string;
op: any;
intervals: Array<[number, number]>;
uses: Array<number>;
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "./phase";
export class TurboshaftGraphPhase extends Phase {
constructor(name: string) {
super(name, PhaseType.TurboshaftGraph);
// To be continued ...
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export class InliningPosition {
sourceId: number;
inliningPosition: SourcePosition;
constructor(sourceId: number, inliningPosition: SourcePosition) {
this.sourceId = sourceId;
this.inliningPosition = inliningPosition;
}
}
export class SourcePosition {
scriptOffset: number;
inliningId: number;
constructor(scriptOffset: number, inliningId: number) {
this.scriptOffset = scriptOffset;
this.inliningId = inliningId;
}
public lessOrEquals(other: SourcePosition): number {
if (this.inliningId == other.inliningId) {
return this.scriptOffset - other.scriptOffset;
}
return this.inliningId - other.inliningId;
}
public equals(other: SourcePosition): boolean {
if (this.scriptOffset != other.scriptOffset) return false;
return this.inliningId == other.inliningId;
}
public isValid(): boolean {
return typeof this.scriptOffset !== undefined && typeof this.inliningId !== undefined;
}
public toString = (): string => `SP:${this.inliningId}:${this.scriptOffset}`;
}
export class BytecodePosition {
bytecodePosition: number;
constructor(bytecodePosition: number) {
this.bytecodePosition = bytecodePosition;
}
public isValid(): boolean {
return typeof this.bytecodePosition !== undefined;
}
public toString = (): string => `BCP:${this.bytecodePosition}`;
}
......@@ -2,7 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { sortUnique, anyToString } from "./common/util";
import { anyToString, camelize, sortUnique } from "./common/util";
import { PhaseType } from "./phases/phase";
import { GraphPhase } from "./phases/graph-phase";
import { DisassemblyPhase } from "./phases/disassembly-phase";
import { BytecodePosition, InliningPosition, SourcePosition } from "./position";
import { CodeOffsetsInfo, InstructionsPhase,
TurbolizerInstructionStartInfo } from "./phases/instructions-phase";
import { SchedulePhase } from "./phases/schedule-phase";
import { SequencePhase } from "./phases/sequence-phase";
import { BytecodeOrigin } from "./origin";
import { Source } from "./source";
import { NodeLabel } from "./node-label";
function sourcePositionLe(a, b) {
......@@ -17,7 +27,7 @@ function sourcePositionEq(a, b) {
a.scriptOffset == b.scriptOffset;
}
export function sourcePositionToStringKey(sourcePosition: AnyPosition): string {
export function sourcePositionToStringKey(sourcePosition: GenericPosition): string {
if (!sourcePosition) return "undefined";
if ('inliningId' in sourcePosition && 'scriptOffset' in sourcePosition) {
return "SP:" + sourcePosition.inliningId + ":" + sourcePosition.scriptOffset;
......@@ -33,72 +43,9 @@ export function sourcePositionValid(l) {
&& typeof l.inliningId !== undefined) || typeof l.bytecodePosition != undefined;
}
export interface SourcePosition {
scriptOffset: number;
inliningId: number;
}
interface TurboFanOrigin {
phase: string;
reducer: string;
}
export interface NodeOrigin {
nodeId: number;
}
interface BytecodePosition {
bytecodePosition: number;
}
export type Origin = NodeOrigin | BytecodePosition;
export type TurboFanNodeOrigin = NodeOrigin & TurboFanOrigin;
export type TurboFanBytecodeOrigin = BytecodePosition & TurboFanOrigin;
type AnyPosition = SourcePosition | BytecodePosition;
export interface Source {
sourcePositions: Array<SourcePosition>;
sourceName: string;
functionName: string;
sourceText: string;
sourceId: number;
startPosition?: number;
backwardsCompatibility: boolean;
}
interface Inlining {
inliningPosition: SourcePosition;
sourceId: number;
}
interface OtherPhase {
type: "disassembly" | "sequence" | "schedule";
name: string;
data: any;
}
interface InstructionsPhase {
type: "instructions";
name: string;
data: any;
instructionOffsetToPCOffset?: any;
blockIdToInstructionRange?: any;
nodeIdToInstructionRange?: any;
codeOffsetsInfo?: CodeOffsetsInfo;
}
interface GraphPhase {
type: "graph";
name: string;
data: any;
highestNodeId: number;
nodeLabelMap: Array<NodeLabel>;
}
type Phase = GraphPhase | InstructionsPhase | OtherPhase;
export interface Schedule {
nodes: Array<any>;
}
type GenericPosition = SourcePosition | BytecodePosition;
type GenericPhase = GraphPhase | DisassemblyPhase | InstructionsPhase
| SchedulePhase | SequencePhase;
export class Interval {
start: number;
......@@ -110,62 +57,17 @@ export class Interval {
}
}
export interface ChildRange {
id: string;
type: string;
op: any;
intervals: Array<[number, number]>;
uses: Array<number>;
}
export interface Range {
child_ranges: Array<ChildRange>;
is_deferred: boolean;
}
export class RegisterAllocation {
fixedDoubleLiveRanges: Map<string, Range>;
fixedLiveRanges: Map<string, Range>;
liveRanges: Map<string, Range>;
constructor(registerAllocation) {
this.fixedDoubleLiveRanges = new Map<string, Range>(Object.entries(registerAllocation.fixed_double_live_ranges));
this.fixedLiveRanges = new Map<string, Range>(Object.entries(registerAllocation.fixed_live_ranges));
this.liveRanges = new Map<string, Range>(Object.entries(registerAllocation.live_ranges));
}
}
export interface Sequence {
blocks: Array<any>;
register_allocation: RegisterAllocation;
}
class CodeOffsetsInfo {
codeStartRegisterCheck: number;
deoptCheck: number;
initPoison: number;
blocksStart: number;
outOfLineCode: number;
deoptimizationExits: number;
pools: number;
jumpTables: number;
}
export class TurbolizerInstructionStartInfo {
gap: number;
arch: number;
condition: number;
}
export class SourceResolver {
nodePositionMap: Array<AnyPosition>;
nodePositionMap: Array<GenericPosition>;
sources: Array<Source>;
inlinings: Array<Inlining>;
inliningsMap: Map<string, Inlining>;
inlinings: Array<InliningPosition>;
inliningsMap: Map<string, InliningPosition>;
positionToNodes: Map<string, Array<string>>;
phases: Array<Phase>;
phases: Array<GenericPhase>;
phaseNames: Map<string, number>;
disassemblyPhase: Phase;
linePositionMap: Map<string, Array<AnyPosition>>;
disassemblyPhase: DisassemblyPhase;
instructionsPhase: InstructionsPhase;
linePositionMap: Map<string, Array<GenericPosition>>;
nodeIdToInstructionRange: Array<[number, number]>;
blockIdToInstructionRange: Array<[number, number]>;
instructionToPCOffset: Array<TurbolizerInstructionStartInfo>;
......@@ -232,7 +134,7 @@ export class SourceResolver {
setInlinings(inlinings) {
if (inlinings) {
for (const [inliningId, inlining] of Object.entries<Inlining>(inlinings)) {
for (const [inliningId, inlining] of Object.entries<InliningPosition>(inlinings)) {
this.inlinings[inliningId] = inlining;
this.inliningsMap.set(sourcePositionToStringKey(inlining.inliningPosition), inlining);
}
......@@ -288,7 +190,7 @@ export class SourceResolver {
return nodeIds;
}
nodeIdsToSourcePositions(nodeIds): Array<AnyPosition> {
nodeIdsToSourcePositions(nodeIds): Array<GenericPosition> {
const sourcePositions = new Map();
for (const nodeId of nodeIds) {
const sp = this.nodePositionMap[nodeId];
......@@ -317,7 +219,7 @@ export class SourceResolver {
return location;
}
addInliningPositions(sourcePosition: AnyPosition, locations: Array<SourcePosition>) {
addInliningPositions(sourcePosition: GenericPosition, locations: Array<SourcePosition>) {
const inlining = this.inliningsMap.get(sourcePositionToStringKey(sourcePosition));
if (!inlining) return;
const sourceId = inlining.sourceId;
......@@ -328,7 +230,7 @@ export class SourceResolver {
}
}
getInliningForPosition(sourcePosition: AnyPosition) {
getInliningForPosition(sourcePosition: GenericPosition) {
return this.inliningsMap.get(sourcePositionToStringKey(sourcePosition));
}
......@@ -388,38 +290,23 @@ export class SourceResolver {
return inliningStack;
}
recordOrigins(phase: GraphPhase) {
if (phase.type != "graph") return;
for (const node of phase.data.nodes) {
phase.highestNodeId = Math.max(phase.highestNodeId, node.id);
if (node.origin != undefined &&
node.origin.bytecodePosition != undefined) {
const position = { bytecodePosition: node.origin.bytecodePosition };
private recordOrigins(graphPhase: GraphPhase): void {
if (graphPhase.type !== PhaseType.Graph) return;
for (const node of graphPhase.data.nodes) {
graphPhase.highestNodeId = Math.max(graphPhase.highestNodeId, node.id);
const origin = node.nodeLabel.origin;
const isBytecode = origin instanceof BytecodeOrigin;
if (isBytecode) {
const position = new BytecodePosition(origin.bytecodePosition);
this.nodePositionMap[node.id] = position;
const key = sourcePositionToStringKey(position);
const key = position.toString();
if (!this.positionToNodes.has(key)) {
this.positionToNodes.set(key, []);
}
const A = this.positionToNodes.get(key);
if (!A.includes(node.id)) A.push(`${node.id}`);
const nodes = this.positionToNodes.get(key);
const identifier = node.identifier();
if (!nodes.includes(identifier)) nodes.push(identifier);
}
// Backwards compatibility.
if (typeof node.pos === "number") {
node.sourcePosition = { scriptOffset: node.pos, inliningId: -1 };
}
}
}
readNodeIdToInstructionRange(nodeIdToInstructionRange) {
for (const [nodeId, range] of Object.entries<[number, number]>(nodeIdToInstructionRange)) {
this.nodeIdToInstructionRange[nodeId] = range;
}
}
readBlockIdToInstructionRange(blockIdToInstructionRange) {
for (const [blockId, range] of Object.entries<[number, number]>(blockIdToInstructionRange)) {
this.blockIdToInstructionRange[blockId] = range;
}
}
......@@ -435,23 +322,6 @@ export class SourceResolver {
return X;
}
readInstructionOffsetToPCOffset(instructionToPCOffset) {
for (const [instruction, numberOrInfo] of Object.entries<number | TurbolizerInstructionStartInfo>(instructionToPCOffset)) {
let info: TurbolizerInstructionStartInfo;
if (typeof numberOrInfo == "number") {
info = { gap: numberOrInfo, arch: numberOrInfo, condition: numberOrInfo };
} else {
info = numberOrInfo;
}
this.instructionToPCOffset[instruction] = info;
if (!this.pcOffsetToInstructions.has(info.gap)) {
this.pcOffsetToInstructions.set(info.gap, []);
}
this.pcOffsetToInstructions.get(info.gap).push(Number(instruction));
}
this.pcOffsets = Array.from(this.pcOffsetToInstructions.keys()).sort((a, b) => b - a);
}
hasPCOffsets() {
return this.pcOffsetToInstructions.size > 0;
}
......@@ -567,51 +437,70 @@ export class SourceResolver {
return [[], []];
}
parsePhases(phases) {
const nodeLabelMap = [];
for (const [, phase] of Object.entries<Phase>(phases)) {
switch (phase.type) {
case 'disassembly':
this.disassemblyPhase = phase;
if (phase['blockIdToOffset']) {
for (const [blockId, pc] of Object.entries<number>(phase['blockIdToOffset'])) {
this.blockIdToPCOffset[blockId] = pc;
if (!this.blockStartPCtoBlockIds.has(pc)) {
this.blockStartPCtoBlockIds.set(pc, []);
}
this.blockStartPCtoBlockIds.get(pc).push(Number(blockId));
}
}
public parsePhases(phasesJson): void {
const nodeLabelMap = new Array<NodeLabel>();
for (const [, genericPhase] of Object.entries<GenericPhase>(phasesJson)) {
switch (genericPhase.type) {
case PhaseType.Disassembly:
const castedDisassembly = genericPhase as DisassemblyPhase;
const disassemblyPhase = new DisassemblyPhase(castedDisassembly.name,
castedDisassembly.data);
disassemblyPhase.parseBlockIdToOffsetFromJSON(castedDisassembly?.blockIdToOffset);
this.disassemblyPhase = disassemblyPhase;
// Will be taken from the class
this.blockIdToPCOffset = disassemblyPhase.blockIdToOffset;
this.blockStartPCtoBlockIds = disassemblyPhase.blockStartPCtoBlockIds;
break;
case 'schedule':
this.phaseNames.set(phase.name, this.phases.length);
this.phases.push(this.parseSchedule(phase));
case PhaseType.Schedule:
const castedSchedule = genericPhase as SchedulePhase;
const schedulePhase = new SchedulePhase(castedSchedule.name, castedSchedule.data);
this.phaseNames.set(schedulePhase.name, this.phases.length);
schedulePhase.parseScheduleFromJSON(castedSchedule.data);
this.phases.push(schedulePhase);
break;
case 'sequence':
this.phaseNames.set(phase.name, this.phases.length);
this.phases.push(this.parseSequence(phase));
case PhaseType.Sequence:
const castedSequence = camelize(genericPhase) as SequencePhase;
const sequencePhase = new SequencePhase(castedSequence.name, castedSequence.blocks,
castedSequence.registerAllocation);
this.phaseNames.set(sequencePhase.name, this.phases.length);
this.phases.push(sequencePhase);
break;
case 'instructions':
if (phase.nodeIdToInstructionRange) {
this.readNodeIdToInstructionRange(phase.nodeIdToInstructionRange);
}
if (phase.blockIdToInstructionRange) {
this.readBlockIdToInstructionRange(phase.blockIdToInstructionRange);
}
if (phase.instructionOffsetToPCOffset) {
this.readInstructionOffsetToPCOffset(phase.instructionOffsetToPCOffset);
}
if (phase.codeOffsetsInfo) {
this.codeOffsetsInfo = phase.codeOffsetsInfo;
}
case PhaseType.Instructions:
const castedInstructions = genericPhase as InstructionsPhase;
let instructionsPhase: InstructionsPhase = null;
if (this.instructionsPhase) {
instructionsPhase = this.instructionsPhase;
instructionsPhase.name += `, ${castedInstructions.name}`;
} else {
instructionsPhase = new InstructionsPhase(castedInstructions.name);
}
instructionsPhase.parseNodeIdToInstructionRangeFromJSON(castedInstructions
?.nodeIdToInstructionRange);
instructionsPhase.parseBlockIdToInstructionRangeFromJSON(castedInstructions
?.blockIdToInstructionRange);
instructionsPhase.parseInstructionOffsetToPCOffsetFromJSON(castedInstructions
?.instructionOffsetToPCOffset);
instructionsPhase.parseCodeOffsetsInfoFromJSON(castedInstructions
?.codeOffsetsInfo);
this.instructionsPhase = instructionsPhase;
// Will be taken from the class
this.nodeIdToInstructionRange = instructionsPhase.nodeIdToInstructionRange;
this.blockIdToInstructionRange = instructionsPhase.blockIdToInstructionRange;
this.codeOffsetsInfo = instructionsPhase.codeOffsetsInfo;
this.instructionToPCOffset = instructionsPhase.instructionToPCOffset;
this.pcOffsetToInstructions = instructionsPhase.pcOffsetToInstructions;
this.pcOffsets = instructionsPhase.pcOffsets;
break;
case 'graph':
const graphPhase: GraphPhase = Object.assign(phase, { highestNodeId: 0 });
case PhaseType.Graph:
const castedGraph = genericPhase as GraphPhase;
const graphPhase = new GraphPhase(castedGraph.name, 0);
graphPhase.parseDataFromJSON(castedGraph.data, nodeLabelMap);
graphPhase.nodeLabelMap = nodeLabelMap.slice();
this.recordOrigins(graphPhase);
this.phaseNames.set(graphPhase.name, this.phases.length);
this.phases.push(graphPhase);
this.recordOrigins(graphPhase);
this.internNodeLabels(graphPhase, nodeLabelMap);
graphPhase.nodeLabelMap = nodeLabelMap.slice();
break;
default:
throw "Unsupported phase type";
......@@ -619,22 +508,6 @@ export class SourceResolver {
}
}
internNodeLabels(phase: GraphPhase, nodeLabelMap: Array<NodeLabel>) {
for (const n of phase.data.nodes) {
const label = new NodeLabel(n.id, n.label, n.title, n.live,
n.properties, n.sourcePosition, n.origin, n.opcode, n.control,
n.opinfo, n.type);
const previous = nodeLabelMap[label.id];
if (!label.equals(previous)) {
if (previous != undefined) {
label.setInplaceUpdatePhase(phase.name);
}
nodeLabelMap[label.id] = label;
}
n.nodeLabel = nodeLabelMap[label.id];
}
}
repairPhaseId(anyPhaseId) {
return Math.max(0, Math.min(anyPhaseId | 0, this.phases.length - 1));
}
......@@ -647,11 +520,11 @@ export class SourceResolver {
return this.phaseNames.get(phaseName);
}
forEachPhase(f: (value: Phase, index: number, array: Array<Phase>) => void) {
forEachPhase(f: (value: GenericPhase, index: number, array: Array<GenericPhase>) => void) {
this.phases.forEach(f);
}
addAnyPositionToLine(lineNumber: number | string, sourcePosition: AnyPosition) {
addAnyPositionToLine(lineNumber: number | string, sourcePosition: GenericPosition) {
const lineNumberString = anyToString(lineNumber);
if (!this.linePositionMap.has(lineNumberString)) {
this.linePositionMap.set(lineNumberString, []);
......@@ -663,7 +536,7 @@ export class SourceResolver {
setSourceLineToBytecodePosition(sourceLineToBytecodePosition: Array<number> | undefined) {
if (!sourceLineToBytecodePosition) return;
sourceLineToBytecodePosition.forEach((pos, i) => {
this.addAnyPositionToLine(i, { bytecodePosition: pos });
this.addAnyPositionToLine(i, new BytecodePosition(pos));
});
}
......@@ -672,96 +545,4 @@ export class SourceResolver {
if (positions === undefined) return [];
return positions;
}
parseSchedule(phase) {
function createNode(state: any, match) {
let inputs = [];
if (match.groups.args) {
const nodeIdsString = match.groups.args.replace(/\s/g, '');
const nodeIdStrings = nodeIdsString.split(',');
inputs = nodeIdStrings.map(n => Number.parseInt(n, 10));
}
const node = {
id: Number.parseInt(match.groups.id, 10),
label: match.groups.label,
inputs: inputs
};
if (match.groups.blocks) {
const nodeIdsString = match.groups.blocks.replace(/\s/g, '').replace(/B/g, '');
const nodeIdStrings = nodeIdsString.split(',');
const successors = nodeIdStrings.map(n => Number.parseInt(n, 10));
state.currentBlock.succ = successors;
}
state.nodes[node.id] = node;
state.currentBlock.nodes.push(node);
}
function createBlock(state, match) {
let predecessors = [];
if (match.groups.in) {
const blockIdsString = match.groups.in.replace(/\s/g, '').replace(/B/g, '');
const blockIdStrings = blockIdsString.split(',');
predecessors = blockIdStrings.map(n => Number.parseInt(n, 10));
}
const block = {
id: Number.parseInt(match.groups.id, 10),
isDeferred: match.groups.deferred != undefined,
pred: predecessors.sort(),
succ: [],
nodes: []
};
state.blocks[block.id] = block;
state.currentBlock = block;
}
function setGotoSuccessor(state, match) {
state.currentBlock.succ = [Number.parseInt(match.groups.successor.replace(/\s/g, ''), 10)];
}
const rules = [
{
lineRegexps:
[/^\s*(?<id>\d+):\ (?<label>.*)\((?<args>.*)\)$/,
/^\s*(?<id>\d+):\ (?<label>.*)\((?<args>.*)\)\ ->\ (?<blocks>.*)$/,
/^\s*(?<id>\d+):\ (?<label>.*)$/
],
process: createNode
},
{
lineRegexps:
[/^\s*---\s*BLOCK\ B(?<id>\d+)\s*(?<deferred>\(deferred\))?(\ <-\ )?(?<in>[^-]*)?\ ---$/
],
process: createBlock
},
{
lineRegexps:
[/^\s*Goto\s*->\s*B(?<successor>\d+)\s*$/
],
process: setGotoSuccessor
}
];
const lines = phase.data.split(/[\n]/);
const state = { currentBlock: undefined, blocks: [], nodes: [] };
nextLine:
for (const line of lines) {
for (const rule of rules) {
for (const lineRegexp of rule.lineRegexps) {
const match = line.match(lineRegexp);
if (match) {
rule.process(state, match);
continue nextLine;
}
}
}
console.log("Warning: unmatched schedule line \"" + line + "\"");
}
phase.schedule = state;
return phase;
}
parseSequence(phase) {
phase.sequence = { blocks: phase.blocks,
register_allocation: phase.register_allocation ? new RegisterAllocation(phase.register_allocation)
: undefined };
return phase;
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { SourcePosition } from "./position";
export class Source {
sourceName: string;
functionName: string;
sourceText: string;
sourceId: number;
backwardsCompatibility: boolean;
sourcePositions: Array<SourcePosition>;
startPosition?: number;
endPosition?: number;
constructor(sourceName: string, functionName: string, sourceText: string, sourceId: number,
backwardsCompatibility: boolean, sourcePositions?: Array<SourcePosition>,
startPosition?: number, endPosition?: number) {
this.sourceName = sourceName;
this.functionName = functionName;
this.sourceText = sourceText;
this.sourceId = sourceId;
this.backwardsCompatibility = backwardsCompatibility;
this.sourcePositions = sourcePositions ?? new Array<SourcePosition>();
this.startPosition = startPosition;
this.endPosition = endPosition;
}
public toString = (): string => `${this.sourceName}:${this.functionName}`;
}
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Source } from "../source";
interface PR {
prettyPrint(_: unknown, el: HTMLElement): void;
}
......@@ -10,7 +12,7 @@ declare global {
const PR: PR;
}
import { Source, SourceResolver, sourcePositionToStringKey } from "../source-resolver";
import { SourceResolver, sourcePositionToStringKey } from "../source-resolver";
import { SelectionBroker } from "../selection/selection-broker";
import { View } from "./view";
import { MySelection } from "../selection/selection";
......
......@@ -264,7 +264,7 @@ export class GraphView extends PhaseView {
const adaptedSelection = this.adaptSelectionToCurrentPhase(data.data, rememberedSelection);
this.phaseName = data.name;
this.createGraph(data.data, adaptedSelection);
this.createGraph(data, adaptedSelection);
this.broker.addNodeHandler(this.selectionHandler);
if (adaptedSelection != null && adaptedSelection.size > 0) {
......
......@@ -4,7 +4,8 @@
import { createElement } from "../common/util";
import { SequenceView } from "./sequence-view";
import { RegisterAllocation, Range, ChildRange, Interval } from "../source-resolver";
import { Interval } from "../source-resolver";
import { ChildRange, Range, RegisterAllocation } from "../phases/sequence-phase";
class Constants {
// Determines how many rows each div group holds for the purposes of
......@@ -300,7 +301,7 @@ class Helper {
}
static fixedRegisterName(range: Range) {
return range.child_ranges[0].op.text;
return range.childRanges[0].op.text;
}
static getPositionElementsFromInterval(interval: HTMLElement) {
......@@ -321,7 +322,7 @@ class Helper {
const entry = fixedRegisterMap.get(registerName);
entry.ranges[1] = range;
// Only use the deferred register index if no normal index exists.
if (!range.is_deferred) {
if (!range.isDeferred) {
entry.registerIndex = parseInt(registerIndex, 10);
}
} else {
......@@ -394,7 +395,7 @@ class RowConstructor {
const intervalMap = new Map<number, HTMLElement>();
let tooltip = "";
ranges.forEachRange((range: Range) => {
for (const childRange of range.child_ranges) {
for (const childRange of range.childRanges) {
switch (childRange.type) {
case "none":
tooltip = Constants.INTERVAL_TEXT_FOR_NONE;
......@@ -417,7 +418,7 @@ class RowConstructor {
childRange.intervals.forEach((intervalNums, index) => {
const interval = new Interval(intervalNums);
const intervalEl = this.elementForInterval(childRange, interval, tooltip,
index, range.is_deferred);
index, range.isDeferred);
intervalMap.set(interval.start, intervalEl);
});
}
......@@ -490,7 +491,7 @@ class RowConstructor {
}
private setUses(grid: Grid, row: number, range: Range) {
for (const liveRange of range.child_ranges) {
for (const liveRange of range.childRanges) {
if (liveRange.uses) {
for (const use of liveRange.uses) {
grid.getCell(row, use).classList.toggle("range-use", true);
......@@ -570,7 +571,7 @@ class RangeViewConstructor {
}
private addVirtualRanges(row: number) {
const source = this.view.sequenceView.sequence.register_allocation;
const source = this.view.sequenceView.sequence.registerAllocation;
for (const [registerIndex, range] of source.liveRanges) {
const registerName = Helper.virtualRegisterName(registerIndex);
const registerEl = this.elementForVirtualRegister(registerName);
......@@ -583,7 +584,7 @@ class RangeViewConstructor {
}
private addFixedRanges(row: number) {
row = Helper.forEachFixedRange(this.view.sequenceView.sequence.register_allocation, row,
row = Helper.forEachFixedRange(this.view.sequenceView.sequence.registerAllocation, row,
(registerIndex: string, row: number,
registerName: string, ranges: RangePair) => {
const registerEl = this.elementForFixedRegister(registerName);
......@@ -765,14 +766,14 @@ class PhaseChangeHandler {
const currentGrid = this.view.gridAccessor.getAnyGrid();
const newGrid = new Grid();
this.view.gridAccessor.addGrid(newGrid);
const source = this.view.sequenceView.sequence.register_allocation;
const source = this.view.sequenceView.sequence.registerAllocation;
let row = 0;
for (const [registerIndex, range] of source.liveRanges) {
this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex,
new RangePair([range, undefined]));
++row;
}
Helper.forEachFixedRange(this.view.sequenceView.sequence.register_allocation, row,
Helper.forEachFixedRange(this.view.sequenceView.sequence.registerAllocation, row,
(registerIndex, row, _, ranges) => {
this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex, ranges);
});
......
......@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Schedule, SourceResolver } from "../source-resolver";
import { SourceResolver } from "../source-resolver";
import { TextView } from "./text-view";
import { SchedulePhase } from "../phases/schedule-phase";
export class ScheduleView extends TextView {
schedule: Schedule;
schedule: SchedulePhase;
sourceResolver: SourceResolver;
createViewElement() {
......@@ -178,7 +179,7 @@ export class ScheduleView extends TextView {
const select = [];
window.sessionStorage.setItem("lastSearch", query);
const reg = new RegExp(query);
for (const node of this.schedule.nodes) {
for (const node of this.schedule.schedule.nodes) {
if (node === undefined) continue;
if (reg.exec(this.lineString(node)) != null) {
select.push(node.id);
......
......@@ -2,13 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Sequence } from "../source-resolver";
import { createElement } from "../common/util";
import { TextView } from "./text-view";
import { RangeView } from "./range-view";
import { SequencePhase } from "../phases/sequence-phase";
export class SequenceView extends TextView {
sequence: Sequence;
sequence: SequencePhase;
searchInfo: Array<any>;
phaseSelect: HTMLSelectElement;
numInstructions: number;
......@@ -80,9 +80,9 @@ export class SequenceView extends TextView {
if (this.showRangeView) this.rangeView.onresize();
}
initializeContent(data, rememberedSelection) {
initializeContent(sequence, rememberedSelection) {
this.divNode.innerHTML = '';
this.sequence = data.sequence;
this.sequence = sequence;
this.searchInfo = [];
this.divNode.onclick = (e: MouseEvent) => {
if (!(e.target instanceof HTMLElement)) return;
......@@ -93,7 +93,6 @@ export class SequenceView extends TextView {
};
this.phaseSelect = (document.getElementById('phase-select') as HTMLSelectElement);
this.currentPhaseIndex = this.phaseSelect.selectedIndex;
this.addBlocks(this.sequence.blocks);
const lastBlock = this.sequence.blocks[this.sequence.blocks.length - 1];
this.numInstructions = lastBlock.instructions[lastBlock.instructions.length - 1].id + 1;
......@@ -313,11 +312,11 @@ export class SequenceView extends TextView {
this.toggleRangeViewEl.setAttribute("title", reason);
};
if (this.sequence.register_allocation) {
if (this.sequence.registerAllocation) {
if (!this.rangeView) {
this.rangeView = new RangeView(this);
}
const source = this.sequence.register_allocation;
const source = this.sequence.registerAllocation;
if (source.fixedLiveRanges.size == 0 && source.liveRanges.size == 0) {
preventRangeView("No live ranges to show");
} else if (this.numInstructions >= 249) {
......
......@@ -17,6 +17,13 @@
"src/common/util.ts",
"src/common/constants.ts",
"src/common/view-elements.ts",
"src/phases/disassembly-phase.ts",
"src/phases/graph-phase.ts",
"src/phases/instructions-phase.ts",
"src/phases/phase.ts",
"src/phases/schedule-phase.ts",
"src/phases/sequence-phase.ts",
"src/phases/turboshaft-graph-phase.ts",
"src/selection/selection.ts",
"src/selection/selection-broker.ts",
"src/selection/selection-handler.ts",
......@@ -27,6 +34,9 @@
"src/views/disassembly-view.ts",
"src/views/text-view.ts",
"src/views/info-view.ts",
"src/origin.ts",
"src/position.ts",
"src/source.ts",
"src/node.ts",
"src/edge.ts",
"src/graph.ts",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment