新增analysis用于分析游戏缺陷

This commit is contained in:
YHH
2020-07-12 14:51:20 +08:00
parent d9bb76c105
commit 032b293085
12 changed files with 2099 additions and 72 deletions

View File

@@ -0,0 +1,69 @@
/**
* 支持标题安全区的布局类。
*/
class Layout {
public clientArea: Rectangle;
public safeArea: Rectangle;
constructor(){
this.clientArea = new Rectangle(0, 0, SceneManager.stage.stageWidth, SceneManager.stage.stageHeight);
this.safeArea = this.clientArea;
}
public place(size: Vector2, horizontalMargin: number, verticalMargine: number, alignment: Alignment){
let rc = new Rectangle(0, 0, size.x, size.y);
if ((alignment & Alignment.left) != 0){
rc.x = this.clientArea.x + (this.clientArea.width * horizontalMargin);
}else if((alignment & Alignment.right) != 0){
rc.x = this.clientArea.x + (this.clientArea.width * (1 - horizontalMargin)) - rc.width;
} else if((alignment & Alignment.horizontalCenter) != 0){
rc.x = this.clientArea.x + (this.clientArea.width - rc.width) / 2 + (horizontalMargin * this.clientArea.width);
}else{
}
if ((alignment & Alignment.top) != 0){
rc.y = this.clientArea.y + (this.clientArea.height * verticalMargine);
}else if((alignment & Alignment.bottom) != 0){
rc.y = this.clientArea.y + (this.clientArea.height * (1 - verticalMargine)) - rc.height;
} else if((alignment & Alignment.verticalCenter) != 0){
rc.y = this.clientArea.y + (this.clientArea.height - rc.height) / 2 + (verticalMargine * this.clientArea.height);
}else{
}
// 确保布局区域在安全区域内。
if (rc.left < this.safeArea.left)
rc.x = this.safeArea.left;
if (rc.right > this.safeArea.right)
rc.x = this.safeArea.right - rc.width;
if (rc.top < this.safeArea.top)
rc.y = this.safeArea.top;
if (rc.bottom > this.safeArea.bottom)
rc.y = this.safeArea.bottom - rc.height;
return rc;
}
}
enum Alignment {
none = 0,
left = 1,
right = 2,
horizontalCenter = 4,
top = 8,
bottom = 16,
verticalCenter = 32,
topLeft = top | left,
topRight = top | right,
topCenter = top | horizontalCenter,
bottomLeft = bottom | left,
bottomRight = bottom | right,
bottomCenter = bottom | horizontalCenter,
centerLeft = verticalCenter | left,
centerRight = verticalCenter | right,
center = verticalCenter | horizontalCenter
}

View File

@@ -0,0 +1,233 @@
namespace stopwatch {
/**
* 记录时间的持续时间,一些设计灵感来自物理秒表。
*/
export class Stopwatch {
/**
* 秒表启动的系统时间。
* undefined如果秒表尚未启动或已复位。
*/
private _startSystemTime: number | undefined;
/**
* 秒表停止的系统时间。
* undefined如果秒表目前没有停止尚未开始或已复位。
*/
private _stopSystemTime: number | undefined;
/** 自上次复位以来,秒表已停止的系统时间总数。 */
private _stopDuration: number = 0;
/**
* 用秒表计时,当前等待的切片开始的时间。
* undefined如果秒表尚未启动或已复位。
*/
private _pendingSliceStartStopwatchTime: number | undefined;
/**
* 记录自上次复位以来所有已完成切片的结果。
*/
private _completeSlices: Slice[] = [];
constructor(private readonly getSystemTime = _defaultSystemTimeGetter) { }
public getState() {
if (this._startSystemTime === undefined) {
return State.IDLE;
} else if (this._stopSystemTime === undefined) {
return State.RUNNING;
} else {
return State.STOPPED;
}
}
public isIdle(){
return this.getState() === State.IDLE;
}
public isRunning(){
return this.getState() === State.RUNNING;
}
public isStopped(){
return this.getState() === State.STOPPED;
}
/**
*
*/
public slice(){
return this.recordPendingSlice();
}
/**
* 获取自上次复位以来该秒表已完成/记录的所有片的列表。
*/
public getCompletedSlices(): Slice[] {
return Array.from(this._completeSlices);
}
/**
* 获取自上次重置以来该秒表已完成/记录的所有片的列表,以及当前挂起的片。
*/
public getCompletedAndPendingSlices(): Slice[] {
return [...this._completeSlices, this.getPendingSlice()];
}
/**
* 获取关于这个秒表当前挂起的切片的详细信息。
*/
public getPendingSlice(): Slice {
return this.calculatePendingSlice();
}
/**
* 获取当前秒表时间。这是这个秒表自上次复位以来运行的系统时间总数。
*/
public getTime(){
return this.caculateStopwatchTime();
}
/**
* 计算指定秒表时间的当前挂起片。
* @param endStopwatchTime
*/
private calculatePendingSlice(endStopwatchTime?: number): Slice {
if (this._pendingSliceStartStopwatchTime === undefined){
return Object.freeze({startTime: 0, endTime: 0, duration: 0});
}
if (endStopwatchTime === undefined){
endStopwatchTime = this.getTime();
}
return Object.freeze({
startTime: this._pendingSliceStartStopwatchTime,
endTime: endStopwatchTime,
duration: endStopwatchTime - this._pendingSliceStartStopwatchTime
});
}
/**
* 计算指定系统时间的当前秒表时间。
* @param endSystemTime
*/
private caculateStopwatchTime(endSystemTime?: number){
if (this._startSystemTime === undefined)
return 0;
if (endSystemTime === undefined)
endSystemTime = this.getSystemTimeOfCurrentStopwatchTime();
return endSystemTime - this._startSystemTime - this._stopDuration;
}
/**
* 获取与当前秒表时间等效的系统时间。
* 如果该秒表当前停止,则返回该秒表停止时的系统时间。
*/
private getSystemTimeOfCurrentStopwatchTime(){
return this._stopSystemTime === undefined ? this.getSystemTime() : this._stopSystemTime;
}
/**
* 完全重置这个秒表到它的初始状态。清除所有记录的运行持续时间、切片等。
*/
public reset(){
this._startSystemTime = this._pendingSliceStartStopwatchTime = this._stopSystemTime = undefined;
this._stopDuration = 0;
this._completeSlices = [];
}
/**
* 开始(或继续)运行秒表。
* @param forceReset
*/
public start(forceReset: boolean = false){
if (forceReset){
this.reset();
}
if (this._stopSystemTime !== undefined){
const systemNow = this.getSystemTime();
const stopDuration = systemNow - this._stopSystemTime;
this._stopDuration += stopDuration;
this._stopSystemTime = undefined;
} else if (this._startSystemTime === undefined){
const systemNow = this.getSystemTime();
this._startSystemTime = systemNow;
this._pendingSliceStartStopwatchTime = 0;
}
}
/**
*
* @param recordPendingSlice
*/
public stop(recordPendingSlice: boolean = false){
if (this._startSystemTime === undefined){
return 0;
}
const systemTimeOfStopwatchTime = this.getSystemTimeOfCurrentStopwatchTime();
if (recordPendingSlice){
this.recordPendingSlice(this.caculateStopwatchTime(systemTimeOfStopwatchTime));
}
this._stopSystemTime = systemTimeOfStopwatchTime;
return this.getTime();
}
/**
* 结束/记录当前挂起的片的私有实现。
* @param endStopwatchTime
*/
private recordPendingSlice(endStopwatchTime?: number){
if (this._pendingSliceStartStopwatchTime !== undefined){
if (endStopwatchTime === undefined){
endStopwatchTime = this.getTime();
}
const slice = this.calculatePendingSlice(endStopwatchTime);
this._pendingSliceStartStopwatchTime = slice.endTime;
this._completeSlices.push(slice);
return slice;
} else {
return this.calculatePendingSlice();
}
}
}
/**
* 返回某个系统的“当前时间”的函数。
* 惟一的要求是,对该函数的每次调用都必须返回一个大于或等于前一次对该函数的调用的数字。
*/
export type GetTimeFunc = () => number;
enum State {
/** 秒表尚未启动,或已复位。 */
IDLE = "IDLE",
/** 秒表正在运行。 */
RUNNING = "RUNNING",
/** 秒表以前还在跑,但现在已经停了。 */
STOPPED = "STOPPED"
}
export function setDefaultSystemTimeGetter(systemTimeGetter: GetTimeFunc = Date.now) {
_defaultSystemTimeGetter = systemTimeGetter;
}
/**
* 由秒表记录的单个“薄片”的测量值
*/
interface Slice {
/** 秒表显示的时间在这一片开始的时候。 */
readonly startTime: number;
/** 秒表在这片片尾的时间。 */
readonly endTime: number;
/** 该切片的运行时间 */
readonly duration: number;
}
/** 所有新实例的默认“getSystemTime”实现 */
let _defaultSystemTimeGetter: GetTimeFunc = Date.now;
}

View File

@@ -0,0 +1,342 @@
/**
* 通过使用这个类您可以直观地找到瓶颈和基本的CPU使用情况。
*/
class TimeRuler {
/** 最大条数 8 */
public static readonly maxBars = 0;
/** */
public static readonly maxSamples = 256;
/** 每条的最大嵌套调用 */
public static readonly maxNestCall = 32;
/** 条的高度(以像素为单位) */
public static readonly barHeight = 8;
/** 最大显示帧 */
public static readonly maxSampleFrames = 4;
/** 持续时间(帧数)为采取抓拍日志。 */
public static readonly logSnapDuration = 120;
public static readonly barPadding = 2;
public static readonly autoAdjustDelay = 30;
public static Instance: TimeRuler;
private _frameKey = 'frame';
private _logKey = 'log';
/** 每帧的日志 */
private _logs: FrameLog[];
/** 当前显示帧计数 */
private sampleFrames: number;
/** 获取/设置目标样本帧。 */
public targetSampleFrames: number;
/** 获取/设置计时器标尺宽度。 */
public width: number;
public enabled: true;
/** TimerRuler画的位置。 */
private _position: Vector2;
/** 上一帧日志 */
private _prevLog: FrameLog;
/** 当前帧日志 */
private _curLog: FrameLog;
/** 当前帧数量 */
private frameCount: number;
/** */
private markers: MarkerInfo[] = [];
/** 秒表用来测量时间。 */
private stopwacth: stopwatch.Stopwatch = new stopwatch.Stopwatch();
/** 从标记名映射到标记id的字典。 */
private _markerNameToIdMap: Map<string, number> = new Map<string, number>();
/**
* 你想在游戏开始时调用StartFrame更新方法。
* 当游戏在固定时间步进模式下运行缓慢时,更新会多次调用。
* 在这种情况下我们应该忽略StartFrame调用。
* 为此我们只需一直跟踪StartFrame调用的次数直到Draw被调用。
*/
private _updateCount: number;
/** */
public showLog = false;
private _frameAdjust: number;
constructor() {
TimeRuler.Instance = this;
this._logs = new Array<FrameLog>(2);
for (let i = 0; i < this._logs.length; ++i)
this._logs[i] = new FrameLog();
this.sampleFrames = this.targetSampleFrames = 1;
this.width = SceneManager.stage.stageWidth * 0.8;
this.onGraphicsDeviceReset();
}
private onGraphicsDeviceReset() {
let layout = new Layout();
this._position = layout.place(new Vector2(this.width, TimeRuler.barHeight), 0, 0.01, Alignment.bottomCenter).location;
}
/**
*
*/
public startFrame() {
// 当这个方法被多次调用时,我们跳过重置帧。
let lock = new LockUtils(this._frameKey);
lock.lock().then(() => {
this._updateCount = parseInt(egret.localStorage.getItem(this._frameKey), 10);
let count = this._updateCount;
count += 1;
egret.localStorage.setItem(this._frameKey, count.toString());
if (this.enabled && (1 < count && count < TimeRuler.maxSampleFrames))
return;
// 更新当前帧日志。
this._prevLog = this._logs[this.frameCount++ & 0x1];
this._curLog = this._logs[this.frameCount & 0x1];
let endFrameTime = this.stopwacth.getTime();
// 更新标记并创建日志。
for (let barIndex = 0; barIndex < this._prevLog.bars.length; ++barIndex) {
let prevBar = this._prevLog.bars[barIndex];
let nextBar = this._curLog.bars[barIndex];
// 重新打开在前一帧中没有调用结束标记的标记。
for (let nest = 0; nest < prevBar.nestCount; ++nest) {
let markerIdx = prevBar.markerNests[nest];
prevBar.markers[markerIdx].endTime = endFrameTime;
nextBar.markerNests[nest] = nest;
nextBar.markers[nest].markerId = prevBar.markers[markerIdx].markerId;
nextBar.markers[nest].beginTime = 0;
nextBar.markers[nest].endTime = -1;
nextBar.markers[nest].color = prevBar.markers[markerIdx].color;
}
// 更新日志标记
for (let markerIdx = 0; markerIdx < prevBar.markCount; ++markerIdx) {
let duration = prevBar.markers[markerIdx].endTime - prevBar.markers[markerIdx].beginTime;
let markerId = prevBar.markers[markerIdx].markerId;
let m = this.markers[markerId];
m.logs[barIndex].color = prevBar.markers[markerIdx].color;
if (!m.logs[barIndex].initialized) {
m.logs[barIndex].min = duration;
m.logs[barIndex].max = duration;
m.logs[barIndex].avg = duration;
m.logs[barIndex].initialized = true;
} else {
m.logs[barIndex].min = Math.min(m.logs[barIndex].min, duration);
m.logs[barIndex].max = Math.min(m.logs[barIndex].max, duration);
m.logs[barIndex].avg += duration;
m.logs[barIndex].avg *= 0.5;
if (m.logs[barIndex].samples++ >= TimeRuler.logSnapDuration) {
m.logs[barIndex].snapMin = m.logs[barIndex].min;
m.logs[barIndex].snapMax = m.logs[barIndex].max;
m.logs[barIndex].snapAvg = m.logs[barIndex].avg;
m.logs[barIndex].samples = 0;
}
}
}
nextBar.markCount = prevBar.nestCount;
nextBar.nestCount = prevBar.nestCount;
}
this.stopwacth.reset();
this.stopwacth.start();
});
}
/**
* 开始测量时间。
* @param markerName
* @param color
*/
public beginMark(markerName: string, color: number, barIndex: number = 0) {
let lock = new LockUtils(this._frameKey);
lock.lock().then(() => {
if (barIndex < 0 || barIndex >= TimeRuler.maxBars)
throw new Error("barIndex argument out of range");
let bar = this._curLog.bars[barIndex];
if (bar.markCount >= TimeRuler.maxSamples) {
throw new Error("exceeded sample count. either set larger number to timeruler.maxsaple or lower sample count");
}
if (bar.nestCount >= TimeRuler.maxNestCall) {
throw new Error("exceeded nest count. either set larger number to timeruler.maxnestcall or lower nest calls");
}
// 获取注册的标记
let markerId = this._markerNameToIdMap.get(markerName);
if (!markerId) {
// 如果此标记未注册,则注册此标记。
markerId = this.markers.length;
this._markerNameToIdMap.set(markerName, markerId);
}
bar.markerNests[bar.nestCount++] = bar.markCount;
bar.markers[bar.markCount].markerId = markerId;
bar.markers[bar.markCount].color = color;
bar.markers[bar.markCount].beginTime = this.stopwacth.getTime();
bar.markers[bar.markCount].endTime = -1;
});
}
/**
*
* @param markerName
* @param barIndex
*/
public endMark(markerName: string, barIndex: number = 0) {
let lock = new LockUtils(this._frameKey);
lock.lock().then(() => {
if (barIndex < 0 || barIndex >= TimeRuler.maxBars)
throw new Error("barIndex argument out of range");
let bar = this._curLog.bars[barIndex];
if (bar.nestCount <= 0) {
throw new Error("call beginMark method before calling endMark method");
}
let markerId = this._markerNameToIdMap.get(markerName);
if (!markerId) {
throw new Error(`Marker ${markerName} is not registered. Make sure you specifed same name as you used for beginMark method`);
}
let markerIdx = bar.markerNests[--bar.nestCount];
if (bar.markers[markerIdx].markerId != markerId) {
throw new Error("Incorrect call order of beginMark/endMark method. beginMark(A), beginMark(B), endMark(B), endMark(A) But you can't called it like beginMark(A), beginMark(B), endMark(A), endMark(B).");
}
bar.markers[markerIdx].endTime = this.stopwacth.getTime();
});
}
/**
* 获取给定bar索引和标记名称的平均时间。
* @param barIndex
* @param markerName
*/
public getAverageTime(barIndex: number, markerName: string) {
if (barIndex < 0 || barIndex >= TimeRuler.maxBars) {
throw new Error("barIndex argument out of range");
}
let result = 0;
let markerId = this._markerNameToIdMap.get(markerName);
if (markerId) {
result = this.markers[markerId].logs[barIndex].avg;
}
return result;
}
/**
*
*/
public resetLog() {
let lock = new LockUtils(this._logKey);
lock.lock().then(() => {
let count = parseInt(egret.localStorage.getItem(this._logKey), 10);
count += 1;
egret.localStorage.setItem(this._logKey, count.toString());
this.markers.forEach(markerInfo => {
for (let i = 0; i < markerInfo.logs.length; ++i){
markerInfo.logs[i].initialized = false;
markerInfo.logs[i].snapMin = 0;
markerInfo.logs[i].snapMax = 0;
markerInfo.logs[i].snapAvg = 0;
markerInfo.logs[i].min = 0;
markerInfo.logs[i].max = 0;
markerInfo.logs[i].avg = 0;
markerInfo.logs[i].samples = 0;
}
});
});
}
public render(position: Vector2 = this._position, width: number = this.width){
egret.localStorage.setItem(this._frameKey, "0");
if (!this.showLog)
return;
let height = 0;
let maxTime = 0;
this._prevLog.bars.forEach(bar => {
if (bar.markCount > 0){
height += TimeRuler.barHeight + TimeRuler.barPadding * 2;
maxTime = Math.max(maxTime, bar.markers[bar.markCount - 1].endTime);
}
})
const frameSpan = 1 / 60 * 1000;
let sampleSpan = this.sampleFrames * frameSpan;
if (maxTime > sampleSpan){
this._frameAdjust = Math.max(0, this._frameAdjust) + 1;
}else{
this._frameAdjust = Math.min(0, this._frameAdjust) - 1;
}
if (Math.max(this._frameAdjust) > TimeRuler.autoAdjustDelay){
this.sampleFrames = Math.min(TimeRuler.maxSampleFrames, this.sampleFrames);
this.sampleFrames = Math.max(this.targetSampleFrames, (maxTime / frameSpan) + 1);
this._frameAdjust = 0;
}
let msToPs = width / sampleSpan;
let startY = position.y - (height - TimeRuler.barHeight);
let y = startY;
// TODO: draw
}
}
/**
* 日志信息
*/
class FrameLog {
public bars: MarkerCollection[];
constructor() {
this.bars = new Array<MarkerCollection>(TimeRuler.maxBars);
for (let i = 0; i < TimeRuler.maxBars; ++i)
this.bars[i] = new MarkerCollection();
}
}
/**
* 标记的集合
*/
class MarkerCollection {
public markers: Marker[] = new Array<Marker>(TimeRuler.maxSamples);
public markCount: number;
public markerNests: number[] = new Array<number>(TimeRuler.maxNestCall);
public nestCount: number;
}
class Marker {
public markerId: number;
public beginTime: number;
public endTime: number;
public color: number;
}
class MarkerInfo {
public name: string;
public logs: MarkerLog[] = new Array<MarkerLog>(TimeRuler.maxBars);
constructor(name) {
this.name = name;
}
}
class MarkerLog {
public snapMin: number;
public snapMax: number;
public snapAvg: number;
public min: number;
public max: number;
public avg: number;
public samples: number;
public color: number;
public initialized: boolean;
}

View File

@@ -0,0 +1,51 @@
const THREAD_ID = `${Math.floor(Math.random() * 1000)}-${Date.now()}`;
const setItem = egret.localStorage.setItem.bind(localStorage);
const getItem = egret.localStorage.getItem.bind(localStorage);
const removeItem = egret.localStorage.removeItem.bind(localStorage);
const nextTick = fn => {
setTimeout(fn, 0);
};
/**
* 利用共享区域实现快速锁
*/
class LockUtils {
private _keyX: string;
private _keyY: string;
constructor(key){
this._keyX = `mutex_key_${key}_X`;
this._keyY = `mutex_key_${key}_Y`;
}
public lock(){
return new Promise((resolve, reject) => {
const fn = () => {
setItem(this._keyX, THREAD_ID);
if (!getItem(this._keyY) === null){
// restart
nextTick(fn);
}
setItem(this._keyY, THREAD_ID);
if (getItem(this._keyX) !== THREAD_ID){
// delay
setTimeout(()=>{
if (getItem(this._keyY) !== THREAD_ID){
// restart
nextTick(fn);
return;
}
// critical section
resolve();
removeItem(this._keyY);
}, 10);
} else {
resolve();
removeItem(this._keyY);
}
};
fn();
});
}
}