优化保存恢复功能

This commit is contained in:
锦恢 2024-10-18 20:25:57 +08:00
parent f7ac311563
commit 08148209e7
19 changed files with 421 additions and 193 deletions

View File

@ -17,14 +17,16 @@
<script>
window.readVcdFile = async () => {
let inputFile = 'test.vcd';
const response = await fetch(inputFile);
let inputVcdFile = 'test.vcd';
let inputViewFile = 'test.vcd.view';
const response = await fetch(inputVcdFile);
const blob = await response.blob();
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = event => {
const arrayBuffer = event.target.result;
resolve([arrayBuffer, inputFile]);
resolve([arrayBuffer, inputVcdFile, inputViewFile]);
};
reader.readAsArrayBuffer(blob);
});

Binary file not shown.

View File

@ -86,10 +86,10 @@ onMounted(async () => {
}
// vcd
const [arrayBuffer, inputFile] = await window.readVcdFile();
const [arrayBuffer, inputFile, inputViewFile] = await window.readVcdFile();
// inputFile
await recoverFromInputFile(inputFile);
await recoverFromInputFile(inputFile, inputViewFile);
const url = await getCrossOriginWorkerURL(window.workerPath);
const worker = new Worker(url, {

View File

@ -3,9 +3,12 @@ console.log('digital-vcd-viewer mode: ' + mode);
let vscode = window.acquireVsCodeApi === undefined ? undefined : acquireVsCodeApi();
import { debounceWrapper } from '@/hook/utils';
import { globalLookup } from '@/hook/global';
import { makeSaveViewPayload } from '@/hook/recover';
import axios from 'axios';
export let saveDelay = 1000;
/**
*
* @param {string} file
@ -27,4 +30,28 @@ export async function saveView(file, payload) {
}
}
export const saveViewApi = debounceWrapper(saveView, 1000);
/**
*
* @param {number} delay
* @returns {(config: import('@/hook/recover').SavePayloadConfig) => void}
*/
function debounceSaveView(delay) {
let timer;
const configPool = {};
return function (config) {
// 记录所有的 payload tag
Object.assign(configPool, config);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
const payload = makeSaveViewPayload(configPool);
const savePath = globalLookup.originVcdViewFile;
saveView(savePath, payload);
}, delay);
}
}
export const saveViewApi = debounceSaveView(saveDelay);

View File

@ -19,11 +19,10 @@
<script setup>
import { emitter, globalLookup } from '@/hook/global';
import { eventHandler, registerWheelEvent } from '@/hook/wave-view';
import { computed, defineComponent, ref, onMounted } from 'vue';
import { computed, defineComponent, ref } from 'vue';
import { time2cursorX, MovingPivot, cursorX2time } from './cursor';
import formatTime from '@/hook/wave-view/format-time';
import { getNearestUserPivot, UserPivots } from './pivot-view';
import { getNearestUserPivot } from './pivot-view';
import { RelativeAxis } from './relative-axis';

View File

@ -17,12 +17,12 @@ import { RelativeAxis } from './relative-axis';
/**
* @description 该数据结构描述了所有通过 makePivot 创建的 用户信标
* @type {Map<number, UserPivot>} number 时间
* @type {Map<number, UserPivot>} number 当前信标在数轴中的刻度
*/
export const UserPivots = reactive(new Map());
/**
* @type {Map<number, UserPivot>} number id
* @type {Map<number, UserPivot>} number idid 是创建的 Unix 时间戳
*/
export const Id2Pivot = reactive(new Map());

View File

@ -0,0 +1,28 @@
import { saveViewApi } from "@/api";
import { globalLookup } from "@/hook/global";
import { reactive } from "vue";
export const controlPanel = reactive({
sections: [
{
iconClass: 'iconfont icon-collections'
},
{
iconClass: 'iconfont icon-setting'
},
{
iconClass: 'iconfont icon-about'
}
],
currentIndex: -1,
click(index) {
if (this.currentIndex === index) {
this.currentIndex = -1;
} else {
this.currentIndex = index;
}
// 保存
saveViewApi({ rightNavIndex: true });
}
});

View File

@ -28,65 +28,32 @@
</div>
</template>
<script>
import { reactive } from 'vue';
<script setup>
import { defineComponent, reactive } from 'vue';
import TreeView from '@/components/treeview';
import Setting from '@/components/setting';
import About from '@/components/about';
import { emitter } from '@/hook/global';
import { controlPanel } from './right-nav';
export default {
name: 'right-nav',
components: {
TreeView,
Setting,
About
},
props: {
topModules: Array
},
setup(props) {
const controlPanel = reactive({
sections: [
{
iconClass: 'iconfont icon-collections'
},
{
iconClass: 'iconfont icon-setting'
},
{
iconClass: 'iconfont icon-about'
}
],
currentIndex: -1,
click(index) {
if (this.currentIndex === index) {
this.currentIndex = -1;
} else {
this.currentIndex = index;
}
// console.log(this.currentIndex);
defineComponent({ name: 'right-nav' });
const props = defineProps({
topModules: {
type: Array,
required: true
}
});
emitter.on('right-nav', index => {
if (controlPanel.currentIndex === index) {
controlPanel.currentIndex = -1;
} else {
controlPanel.currentIndex = index;
}
})
});
return {
props,
controlPanel
};
}
}
</script>
<style>

View File

@ -1,14 +1,10 @@
import { globalLookup } from "@/hook/global";
import { updateCurrentGroups } from "./manage-group";
import { makeSaveViewPayload } from "@/hook/recover";
import { saveViewApi } from "@/api";
export function onUpdate() {
globalLookup.render();
updateCurrentGroups();
// 保存视图
// TODO: 优化:仅保存视图
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ views: true });
}

View File

@ -2,7 +2,6 @@ import { globalLookup } from "@/hook/global";
import { colorPicker, contextmenu, groupcontextmenu, handleGroupContextmenuFromSignalItem } from "./handle-contextmenu";
import { reactive, ref } from "vue";
import { findViewIndexByLink } from "@/hook/wave-container-view";
import { makeSaveViewPayload } from "@/hook/recover";
import { saveViewApi } from "@/api";
export const groupColors = [
@ -138,10 +137,7 @@ export function createGroup() {
updateCurrentGroups();
// 保存视图
// TODO: 优化:仅保存视图
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ views: true });
}
export function cancelGroup() {
@ -168,10 +164,7 @@ export function cancelGroup() {
updateCurrentGroups();
// 保存视图
// TODO: 优化:仅保存视图
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ views: true });
}
@ -203,7 +196,5 @@ export function deleteGroup() {
updateCurrentGroups();
// 保存视图
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ waves: true });
}

View File

@ -43,9 +43,8 @@
<script setup>
import { saveViewApi } from '@/api';
import { globalLookup } from '@/hook/global';
import { makeSaveViewPayload } from '@/hook/recover';
import { sidebarSelectedWires } from '@/hook/sidebar-select-wire';
import { defineComponent, nextTick, ref, watch } from 'vue';
import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
// 线
@ -71,10 +70,7 @@ function onSignalModalUpdate() {
globalLookup.render();
//
// TODO: waves
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ waves: true });
}
}

View File

@ -26,9 +26,8 @@
<script setup>
import { saveViewApi } from '@/api';
import { globalLookup } from '@/hook/global';
import { makeSaveViewPayload } from '@/hook/recover';
import { sidebarSelectedWires } from '@/hook/sidebar-select-wire';
import { defineComponent, onMounted, reactive, ref, watch } from 'vue';
import { defineComponent, onMounted, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
//
@ -110,10 +109,7 @@ function onChange() {
globalLookup.render({ type: 'value' });
//
// TODO: waves
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ waves: true });
}
}

View File

@ -0,0 +1,5 @@
/**
* @type {Set<ScopeItem>}
*/
export const TreeviewExpandSignals = new Set();

View File

@ -22,18 +22,22 @@
</div>
</template>
<script>
<script setup>
/* eslint-disable */
import { reactive, onMounted } from 'vue';
import { reactive, defineComponent } from 'vue';
import { emitter, globalLookup } from '@/hook/global';
import { isScope, isVariable, makeIconClass } from '@/hook/utils';
import { TreeviewExpandSignals } from './modules';
import { saveViewApi } from '@/api';
defineComponent({ name: 'modules' });
const props = defineProps({
module: {
type: Object,
required: true
}
});
export default {
name: "modules",
props: {
module: Object
},
setup(props) {
const module = props.module;
globalLookup.initcurrentModule(module);
@ -54,27 +58,32 @@ export default {
globalLookup.currentModule = module;
}
function getExpandStatus() {
if (mods.length === 0) {
return false;
}
return TreeviewExpandSignals.has(module);
}
const expandManage = reactive({
expanded: false,
expanded: getExpandStatus(),
expandTagClass: mods.length === 0 ? '' : 'collapse-tag',
click() {
this.expanded = !this.expanded;
if (this.expandTagClass) {
this.expandTagClass = this.expanded ? 'expand-tag' : 'collapse-tag';
}
if (this.expanded) {
TreeviewExpandSignals.add(module);
} else {
TreeviewExpandSignals.delete(module);
}
//
saveViewApi({ treeviewExpands: true });
}
});
return {
module,
mods,
clickItem,
expandManage,
globalLookup,
makeIconClass
}
}
};
</script>
<style>

View File

@ -28,7 +28,6 @@ import { makeIconClass } from '@/hook/utils';
import { WaveContainerView } from '@/hook/wave-container-view';
import { controller } from './signals';
import { saveViewApi } from '@/api';
import { makeSaveViewPayload } from '@/hook/recover';
defineComponent({ name: 'signals' });
@ -91,9 +90,7 @@ function toggleRender(event, signal) {
}
//
const savePath = globalLookup.originFile + '.view';
const payload = makeSaveViewPayload();
saveViewApi(savePath, payload);
saveViewApi({ waves: true });
}
</script>

View File

@ -13,11 +13,16 @@ export const globalLookup = reactive({
/**
* @description 读取的文件的路径
*/
originFile: '',
originVcdFile: '',
/**
* @description 读取文件的 view 路径
*/
originVcdViewFile: '',
/**
* @description 所有的顶层文件
* @type {TopModWireItem[]}
* @type {ScopeItem[]}
*/
topModules: [],
@ -112,16 +117,23 @@ export const globalLookup = reactive({
xScale: 1,
/**
* @description 初始化 module为每一个 module 配上 parent 并给出对应的 nameClass
* @param {ScopeItem} module
*/
initcurrentModule(module) {
if (this.currentModule === undefined && module) {
this.currentModule = module;
}
if (module.nameClass === undefined) {
module.nameClass = module.name;
}
// 创造 parent当然这一步也可能在 recoverSession 中被实现了
if (module.body && module.body instanceof Array) {
for (const childModule of module.body) {
if (childModule.parent === undefined) {
// 计算 nameClass
childModule.parent = module;
}
childModule.nameClass = module.nameClass + '.' + childModule.name;
}
}
},

View File

@ -3,16 +3,40 @@ import { globalLookup } from "./global";
import { WaveContainerView } from "./wave-container-view";
import { isScope, isVariable } from "./utils";
import { BSON } from "bson";
import { Id2Pivot, orderedTimes, UserPivots } from "@/components/pivot/pivot-view";
import { TreeviewExpandSignals } from "@/components/treeview/modules";
import { controlPanel } from "@/components/right-nav";
export const recoverConfig = {
/**
* @type {Map<string, IRenderOption>} 其中的 string 是形如 `cpu.alu.add1` 这样的字符串
*/
waves: new Map(),
/**
* @type {WaveRenderSidebarItem[] | undefined}
*/
views: undefined
views: undefined,
/**
* @type {Pstate | undefined}
*/
state: undefined,
/**
* @type {import("@/components/pivot/pivot-view").UserPivot[] | undefined}
*/
pivots: undefined,
/**
* @type {Set<string> | undefined} nameClass 的集合代表所有需要打开的 treeview 中的 scope
*/
treeviewExpands: undefined,
/**
* @type {number} 右侧的面版的打开的编号-1 代表没有面板被打开
*/
rightNavIndex: -1
};
async function findRecoverFile(recoverJsonPath) {
@ -33,12 +57,30 @@ async function findRecoverFile(recoverJsonPath) {
/**
*
* @param {string} inputFile 读取的 vcd 文件的路径
* @param {string} inputViewFile 读取的 vcd view 文件的路径它如果不存在则自动创建
*/
export async function recoverFromInputFile(inputFile) {
globalLookup.originFile = inputFile;
const recoverJsonPath = inputFile + '.view';
const recoverJson = await findRecoverFile(recoverJsonPath);
async function attemptRecover(inputFile, inputViewFile) {
let recoverResult = await findRecoverFile(inputViewFile);
if (recoverResult !== undefined) {
globalLookup.originVcdViewFile = inputViewFile;
return recoverResult;
}
globalLookup.originVcdViewFile = inputFile + '.view';
return await findRecoverFile(globalLookup.originVcdViewFile);
}
/**
*
* @param {string} inputFile 读取的 vcd 文件的路径
* @param {string} inputViewFile 读取的 vcd view 文件的路径它如果不存在则自动创建
*/
export async function recoverFromInputFile(inputFile, inputViewFile) {
globalLookup.originVcdFile = inputFile;
// 先尝试从 inputViewFile 中寻找,如果找不到,则从同名的 .view 中寻找
const recoverJson = await attemptRecover(inputFile, inputViewFile);
if (recoverJson) {
// 加载 waves
const waves = recoverJson.waves;
if (waves instanceof Array && waves.length > 0) {
for (const wave of waves) {
@ -51,10 +93,32 @@ export async function recoverFromInputFile(inputFile) {
}
}
// 加载 views
const views = recoverJson.views;
if (views instanceof Array && views.length > 0) {
recoverConfig.views = recoverJson.views;
}
// 加载 state
recoverConfig.state = recoverJson.state;
// 加载 pivots
const pivots = recoverJson.pivots;
if (pivots instanceof Array && pivots.length > 0) {
recoverConfig.pivots = pivots;
}
// 加载 treeviewExpands
const treeviewExpands = recoverJson.treeviewExpands;
if (treeviewExpands instanceof Array && treeviewExpands.length > 0) {
recoverConfig.treeviewExpands = treeviewExpands;
}
// 加载 rightNavIndex
const rightNavIndex = recoverJson.rightNavIndex;
if (rightNavIndex !== undefined) {
recoverConfig.rightNavIndex = rightNavIndex;
}
}
}
@ -69,15 +133,12 @@ class PrefixNode {
/**
* @description 恢复现场的函数主要恢复两个变量 currentWiresRenderView currentSignalRenderOptions
* @param {TopModWireItem[]} topModules
* @param {ScopeItem[]} topModules
*/
export function recoverSession(topModules) {
const waves = recoverConfig.waves;
if (waves.size === 0) {
return;
}
// 恢复 waves
// 利用前缀树 记忆化搜索
const waves = recoverConfig.waves;
for (const [name, option] of waves.entries()) {
const result = names2wire(topModules, name);
if (result) {
@ -90,6 +151,7 @@ export function recoverSession(topModules) {
}
}
// 恢复 views
// 如果存在 views
if (recoverConfig.views !== undefined) {
const renderViews = globalLookup.currentWiresRenderView;
@ -120,12 +182,57 @@ export function recoverSession(topModules) {
}
}
// 恢复 state
const pstate = recoverConfig.state;
if (pstate !== undefined) {
Object.assign(globalLookup.pstate, pstate);
}
// 恢复 pivots
const pivots = recoverConfig.pivots;
if (pivots !== undefined && pivots instanceof Array) {
for (const pivot of pivots) {
const time = pivot.time;
const id = pivot.id;
UserPivots.set(time, pivot);
Id2Pivot.set(id, pivot);
orderedTimes.push(time);
}
orderedTimes.sort((a, b) => a < b);
}
// 恢复 treeviewExpands
const treeviewExpands = recoverConfig.treeviewExpands;
if (treeviewExpands !== undefined && treeviewExpands instanceof Array) {
for (const nameClass of treeviewExpands) {
const scope = names2wire(topModules, nameClass);
if (scope !== undefined) {
TreeviewExpandSignals.add(scope);
} else {
console.log('fail to recover [treeviewExpands] item: ' + nameClass);
}
}
}
// 恢复 rightNavIndex
const rightNavIndex = recoverConfig.rightNavIndex;
if (rightNavIndex !== undefined) {
controlPanel.currentIndex = rightNavIndex;
}
globalLookup.render();
recoverConfig.pivots = null;
recoverConfig.state = null;
recoverConfig.views = null;
recoverConfig.treeviewExpands = null;
recoverConfig.waves = null;
recoverConfig.rightNavIndex = null;
}
/**
*
* @param {TopModWireItem[]} topModules
* @param {ScopeItem[]} topModules
* @param {string} name
* @returns {WireItem | undefined}
*/
@ -144,13 +251,17 @@ function names2wire(topModules, name) {
break;
}
if (isScope(targetNode)) {
nodes = targetNode.body;
} else if (isVariable(targetNode)) {
// 搜索完成直接返回
if (layer === deps.length - 1) {
result = targetNode;
if (result.parent === undefined) {
result.parent = lastNode;
}
break;
}
if (isScope(targetNode)) {
nodes = targetNode.body;
} else {
break;
}
@ -173,6 +284,10 @@ function link2names(link) {
const names = [];
const signal = globalLookup.link2CurrentWires.get(link);
if (signal) {
if (signal.nameClass !== undefined) {
return signal.nameClass;
}
let node = signal;
while (node !== undefined) {
names.push(node.name);
@ -195,16 +310,64 @@ function stableClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
export function makeSaveViewPayload() {
const waves = [];
const views = [];
/**
* @typedef {Object} SavePayloadConfig
* @property {boolean} waves
* @property {boolean} views
* @property {boolean} state
* @property {boolean} pivots
* @property {boolean} treeviewExpands
* @property {boolean} rightNavIndex
*/
/**
* @param {SavePayloadConfig} config
* @param {keyof SavePayloadConfig} property
* @returns
*/
function defaultFalseWrapper(config, property) {
if (config === undefined) {
return false;
}
if (config[property] === undefined) {
return false;
}
return Boolean(config[property]);
}
/**
*
* @param {SavePayloadConfig | undefined} config
* @returns
*/
export function makeSaveViewPayload(config) {
const payload = {};
config = config || {};
// 波形 profile
if (defaultFalseWrapper(config, 'waves')) {
// 有 waves 也一定要有 views
config.views = true;
const waves = [];
for (const wire of globalLookup.currentWires) {
const option = globalLookup.currentSignalRenderOptions.get(wire.link) || {};
const name = link2names(wire.link);
option.highlight = 0;
waves.push({ name, option });
}
Object.assign(payload, { waves });
}
// 视图列表
if (defaultFalseWrapper(config, 'views')) {
const views = [];
for (const view of globalLookup.currentWiresRenderView) {
// link 转换成 names
const saveView = stableClone(view);
@ -220,5 +383,42 @@ export function makeSaveViewPayload() {
}
views.push(saveView);
}
return { waves, views };
Object.assign(payload, { views });
}
// 窗口状态
if (defaultFalseWrapper(config, 'state')) {
const state = globalLookup.pstate;
Object.assign(payload, { state });
}
// 信标相关
if (defaultFalseWrapper(config, 'pivots')) {
const pivots = [];
for (const pivot of UserPivots.values()) {
pivots.push(pivot);
}
Object.assign(payload, { pivots });
}
// 右侧信号展开信息
if (defaultFalseWrapper(config, 'treeviewExpands')) {
const treeviewExpands = [];
for (const signal of TreeviewExpandSignals) {
treeviewExpands.push(signal.nameClass);
}
Object.assign(payload, { treeviewExpands });
}
// 右侧控制面板打开序号
if (defaultFalseWrapper(config, 'rightNavIndex')) {
const rightNavIndex = controlPanel.currentIndex;
Object.assign(payload, { rightNavIndex });
}
return payload;
}

View File

@ -46,7 +46,8 @@
* @property {string} link
* @property {string} name
* @property {number} size
* @property {WireItem | undefined} parent
* @property {string} nameClass
* @property {ScopeItem | undefined} parent
* @property {string} type
*/
@ -60,14 +61,16 @@
/**
* @description
* @typedef {Object} TopModWireItem
* @description Scope
* @typedef {Object} ScopeItem
* @property {string} kind
* @property {string} link
* @property {string} name
* @property {number} size
* @property {string} type
* @property {(TopModWireItem | WireItem)[]} body
* @property {string} nameClass
* @property {ScopeItem | undefined} parent
* @property {(ScopeItem | WireItem)[]} body
*/
/**