diff --git a/src/baseplatform.ts b/src/baseplatform.ts index 676b5eba..6afd51e1 100644 --- a/src/baseplatform.ts +++ b/src/baseplatform.ts @@ -98,6 +98,9 @@ export interface Platform { showHelp?(tool:string, ident?:string) : void; resize?() : void; + startProfiling?() : ProfilerOutput; + getRasterScanline?() : number; + debugSymbols? : DebugSymbols; } @@ -122,6 +125,17 @@ export interface EmuRecorder { recordFrame(state : EmuState); } +export interface ProfilerScanline { + start,end : number; // start/end frameindex +} +export interface ProfilerFrame { + iptab : Uint32Array; // array of IPs + lines : ProfilerScanline[]; +} +export interface ProfilerOutput { + frame : ProfilerFrame; +} + ///// export abstract class BasePlatform { @@ -194,10 +208,31 @@ export abstract class BaseDebugPlatform extends BasePlatform { this.advance(novideo); this.postFrame(); } + startProfiling() : ProfilerOutput { + var frame = null; + var output = {frame:null}; + var i = 0; + var lastsl = 9999; + var start = 0; + (this as any).runEval((c:CpuState) => { + var sl = (this as any).getRasterScanline(); + if (sl != lastsl) { + if (frame) frame.lines.push({start:start,end:i}); + if (sl < lastsl) { + output.frame = frame; + frame = {iptab:new Uint32Array(14672), lines:[]}; + i = 0; + } + start = i+1; + lastsl = sl; + } + frame.iptab[i++] = c.EPC || c.PC; + return false; // profile forever + }); + return output; + } } -////// - ////// 6502 export function getToolForFilename_6502(fn:string) : string { @@ -933,7 +968,7 @@ export function dumpStackToString(platform:Platform, mem:Uint8Array|number[], st var opcode = read(addr + jsrofs); // might be out of bounds if (opcode == jsrop) { // JSR s += "\n$" + hex(sp) + ": "; - s += hex(addr,4) + " " + lookupSymbol(platform, addr); + s += hex(addr,4) + " " + lookupSymbol(platform, addr, true); sp++; nraw = 0; } else { @@ -946,20 +981,18 @@ export function dumpStackToString(platform:Platform, mem:Uint8Array|number[], st return s+"\n"; } -export function lookupSymbol(platform:Platform, addr:number) { +export function lookupSymbol(platform:Platform, addr:number, extra:boolean) { var start = addr; - var foundsym; var addr2symbol = platform.debugSymbols && platform.debugSymbols.addr2symbol; while (addr2symbol && addr >= 0) { var sym = addr2symbol[addr]; - if (sym && sym.startsWith('_')) { // return first C symbol we find - return addr2symbol[addr] + " + " + (start-addr); - } else if (sym && !foundsym) { // cache first non-C symbol found - foundsym = sym; + if (sym) { // return first symbol we find + var sym = addr2symbol[addr]; + return extra ? (sym + " + " + (start-addr)) : sym; } addr--; } - return foundsym || ""; + return ""; } ///// Basic Platforms @@ -975,7 +1008,7 @@ export abstract class BasicZ80ScanlinePlatform extends BaseZ80Platform { cpuCyclesPerLine : number; currentScanline : number; startLineTstates : number; - + cpu; membus : MemoryBus; iobus : MemoryBus; @@ -995,12 +1028,12 @@ export abstract class BasicZ80ScanlinePlatform extends BaseZ80Platform { abstract getKeyboardMap(); abstract startScanline(sl : number); abstract drawScanline(sl : number); - + constructor(mainElement : HTMLElement) { super(); this.mainElement = mainElement; } - + start() { this.cpuCyclesPerLine = Math.round(this.cpuFrequency / 60 / this.numTotalScanlines); this.ram = this.newRAM(); @@ -1074,4 +1107,3 @@ export abstract class BasicZ80ScanlinePlatform extends BaseZ80Platform { this.cpu.setTstates(0); } } - diff --git a/src/platform/nes.ts b/src/platform/nes.ts index c8de1348..03fc7681 100644 --- a/src/platform/nes.ts +++ b/src/platform/nes.ts @@ -1,6 +1,6 @@ "use strict"; -import { Platform, Base6502Platform, BaseMAMEPlatform, getOpcodeMetadata_6502, cpuStateToLongString_6502, getToolForFilename_6502, dumpStackToString } from "../baseplatform"; +import { Platform, Base6502Platform, BaseMAMEPlatform, getOpcodeMetadata_6502, cpuStateToLongString_6502, getToolForFilename_6502, dumpStackToString, ProfilerOutput } from "../baseplatform"; import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap, dumpRAM, KeyFlags } from "../emu"; import { hex, lpad, lzgmini } from "../util"; import { CodeAnalyzer_nes } from "../analysis"; @@ -131,9 +131,7 @@ const _JSNESPlatform = function(mainElement) { nes.cpu._emulate = nes.cpu.emulate; nes.cpu.emulate = () => { var cycles = nes.cpu._emulate(); - //if (self.debugCondition && !self.debugBreakState && self.debugClock < 100) console.log(self.debugClock, nes.cpu.REG_PC); this.evalDebugCondition(); - // TODO: doesn't stop on breakpoint return cycles; } timer = new AnimationTimer(60, this.nextFrame.bind(this)); @@ -227,7 +225,35 @@ const _JSNESPlatform = function(mainElement) { runToVsync() { var frame0 = frameindex; - this.runEval(function(c) { return frameindex>frame0; }); + this.runEval((c) => { return frameindex>frame0; }); + } + + getRasterScanline() : number { + return nes.ppu.scanline; + } + startProfiling() : ProfilerOutput { + var frame0 = frameindex; + var frame = null; + var output = {frame:null}; + var i = 0; + var lastsl = 9999; + var start = 0; + this.runEval((c) => { + var sl = this.getRasterScanline(); + if (sl != lastsl) { + if (frame) frame.lines.push({start:start,end:i}); + if (sl < lastsl) { + output.frame = frame; + frame = {iptab:new Uint32Array(14672), lines:[]}; + i = 0; + } + start = i+1; + lastsl = sl; + } + frame.iptab[i++] = c.EPC || c.PC; + return false; //frameindex>frame0; // TODO + }); + return output; } getCPUState() { diff --git a/src/platform/vcs.ts b/src/platform/vcs.ts index ecea88f1..bcac76cd 100644 --- a/src/platform/vcs.ts +++ b/src/platform/vcs.ts @@ -112,6 +112,9 @@ class VCSPlatform extends BasePlatform { var ypos = row-39; return {x:xpos, y:ypos}; } + getRasterScanline() : number { + return this.getRasterPosition().y; + } // TODO: Clock changes this on event, so it may not be current isRunning() { @@ -286,14 +289,14 @@ class VCSPlatform extends BasePlatform { disassemble(pc:number, read:(addr:number)=>number) : DisasmLine { return disassemble6502(pc, read(pc), read(pc+1), read(pc+2)); } - + showHelp(tool:string, ident:string) { if (tool == 'bataribasic') window.open("help/bataribasic/manual.html", "_help"); else window.open("https://alienbill.com/2600/101/docs/stella.html", "_help"); // TODO } - + }; // TODO: mixin for Base6502Platform? diff --git a/src/project.ts b/src/project.ts index a74ac78c..8efd61f4 100644 --- a/src/project.ts +++ b/src/project.ts @@ -209,20 +209,18 @@ export class CodeProject { webpath += ".a"; // legacy stuff // try to GET file, use file ext to determine text/binary this.callbackGetRemote( webpath, (data:FileData) => { - if (data instanceof ArrayBuffer) - data = new Uint8Array(data); // convert to typed array - console.log("GET",webpath,data.length,'bytes'); - this.filedata[path] = data; // do not update store, just cache - addResult(path, data); - loadNext(); - }, isProbablyBinary(path) ? 'arraybuffer' : 'text') - .fail( (err:XMLHttpRequest) => { - console.log("Could not load preset file", path, err.status); - // only cache result if status is 404 (not found) - if (err.status && err.status == 404) + if (data == null) { + console.log("Could not load preset file", path); this.filedata[path] = null; // mark cache entry as invalid + } else { + if (data instanceof ArrayBuffer) + data = new Uint8Array(data); // convert to typed array + console.log("GET",webpath,data.length,'bytes'); + this.filedata[path] = data; // do not update store, just cache + addResult(path, data); + } loadNext(); - }); + }, isProbablyBinary(path) ? 'arraybuffer' : 'text'); } else { // not gonna find it, keep going loadNext(); diff --git a/src/ui.ts b/src/ui.ts index d73e851b..68cb2a9d 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -100,7 +100,12 @@ function getWithBinary(url:string, success:(text:FileData)=>void, datatype:'text oReq.open("GET", url, true); oReq.responseType = datatype; oReq.onload = function (oEvent) { - success(oReq.response); + if (oReq.status == 200) + success(oReq.response); + else if (oReq.status == 404) + success(null); + else + throw "Error " + oReq.status + " loading " + url; } oReq.send(null); } @@ -210,6 +215,11 @@ function refreshWindowList() { return new Views.MemoryMapView(); }); } + if (platform.startProfiling && platform.runEval && platform.getRasterScanline) { + addWindowItem("#profiler", "Profiler", function() { + return new Views.ProfileView(); + }); + } } // can pass integer or string id diff --git a/src/views.ts b/src/views.ts index a5ef78af..898368df 100644 --- a/src/views.ts +++ b/src/views.ts @@ -4,7 +4,7 @@ import $ = require("jquery"); //import CodeMirror = require("codemirror"); import { CodeProject } from "./project"; import { SourceFile, WorkerError, Segment } from "./workertypes"; -import { Platform, EmuState } from "./baseplatform"; +import { Platform, EmuState, ProfilerOutput, lookupSymbol } from "./baseplatform"; import { hex, lpad, rpad } from "./util"; import { CodeAnalyzer } from "./analysis"; import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows } from "./ui"; @@ -610,7 +610,7 @@ export class MemoryView implements ProjectView { if (compparams && this.dumplines) this.scrollToAddress(compparams.data_start); } - + scrollToAddress(addr : number) { this.memorylist.scrollToItem(this.findMemoryWindowLine(addr)); } @@ -774,7 +774,7 @@ export class BinaryFileView implements ProjectView { refresh() { } - + getPath() { return this.path; } } @@ -789,7 +789,7 @@ export class MemoryMapView implements ProjectView { this.refresh(); return this.maindiv[0]; } - + // TODO: overlapping segments (e.g. ROM + LC) addSegment(seg : Segment) { var offset = $('
'); @@ -831,5 +831,84 @@ export class MemoryMapView implements ProjectView { } } } - + +} + +/// + +export class ProfileView implements ProjectView { + profilelist; + prof : ProfilerOutput; + maindiv : HTMLElement; + symcache : {}; + + createDiv(parent : HTMLElement) { + var div = document.createElement('div'); + div.setAttribute("class", "memdump"); + parent.appendChild(div); + this.showMemoryWindow(parent, div); + return this.maindiv = div; + } + + showMemoryWindow(workspace:HTMLElement, parent:HTMLElement) { + this.profilelist = new VirtualList({ + w: $(workspace).width(), + h: $(workspace).height(), + itemHeight: getVisibleEditorLineHeight(), + totalRows: 262, + generatorFn: (row : number) => { + var s = this.getProfileLineAt(row); + var linediv = document.createElement("div"); + linediv.appendChild(document.createTextNode(s)); + return linediv; + } + }); + $(parent).append(this.profilelist.container); + this.symcache = {}; + this.tick(); + } + + getProfileLineAt(row : number) : string { + var s = lpad(row+': ',5); + if (!this.prof) return s; + var f = this.prof.frame; + if (!f) return s; + var l = f.lines[row]; + if (!l) return s; + var lastsym = ''; + for (var i=l.start; i<=l.end; i++) { + var pc = f.iptab[i]; + var sym = this.symcache[pc]; + if (!sym) { + sym = lookupSymbol(platform, pc, false); + this.symcache[pc] = sym; + } + if (sym != lastsym) { + s += sym + ' '; + lastsym = sym; + } + } + return s; + } + + refresh() { + this.tick(); + } + + tick() { + if (this.profilelist) { + $(this.maindiv).find('[data-index]').each( (i,e) => { + var div = $(e); + var row = parseInt(div.attr('data-index')); + var oldtext = div.text(); + var newtext = this.getProfileLineAt(row); + if (oldtext != newtext) + div.text(newtext); + }); + } + // TODO: better way to keep it profiling? also, it clears the buffer + if (platform.isRunning()) { + this.prof = platform.startProfiling(); + } + } } diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts index 03f7b5c4..40b124e3 100644 --- a/src/worker/workermain.ts +++ b/src/worker/workermain.ts @@ -934,8 +934,10 @@ function linkLD65(step:BuildStep) { var toks = s.split(" "); if (toks[0] == 'al') { let ident = toks[2].substr(1); - let ofs = parseInt(toks[1], 16); - symbolmap[ident] = ofs; + if (ident.length != 5 || !ident.startsWith('L')) { // no line numbers + let ofs = parseInt(toks[1], 16); + symbolmap[ident] = ofs; + } } } // build segment map @@ -1226,7 +1228,7 @@ function linkSDLDZ80(step:BuildStep) var symbolmap = {}; for (var s of noiout.split("\n")) { var toks = s.split(" "); - if (toks[0] == 'DEF' && !toks[1].startsWith("A$main$")) { + if (toks[0] == 'DEF' && !toks[1].startsWith("A$")) { symbolmap[toks[1]] = parseInt(toks[2], 16); } } diff --git a/test/cli/teststore.js b/test/cli/teststore.js index ebd36148..d3252845 100644 --- a/test/cli/teststore.js +++ b/test/cli/teststore.js @@ -76,7 +76,10 @@ describe('Store', function() { var platform = {}; var project = new prj.CodeProject(worker, test_platform_id, platform, store); var remote = []; - project.callbackGetRemote = function(path) { remote.push(path); return {fail:function(failfn){failfn({status:404})}} }; + project.callbackGetRemote = function(path, success, datatype) { + remote.push(path); + success(); + }; project.loadFiles(['local/test','test'], function(err, result) { assert.equal(null, err); assert.deepEqual(["presets/_TEST/test"], remote); @@ -85,7 +88,7 @@ describe('Store', function() { }); }); }); - + it('Should build linked project', function(done) { localStorage.clear(); localItems['__migrated__TEST'] = 'true'; @@ -93,7 +96,7 @@ describe('Store', function() { var expectmsgs = [ true, { preload: 'dasm', platform: '_TEST' }, - { + { buildsteps: [ { path: "test.a", platform: "_TEST", tool: "dasm", mainfile:true, files:["test.a"] }, ],