Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance optimizations #52

Merged
merged 3 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"d3": "^7.0.4",
"deep-equal": "^2.0.5",
"fast-deep-equal": "^3.1.3",
"glob-parent": ">=5.1.2"
}
}
10 changes: 5 additions & 5 deletions src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface Note {
id: string;
parent_id: string;
title: string;
links: string[];
links: Set<string>;
linkedToCurrentNote?: boolean;
}

Expand Down Expand Up @@ -140,7 +140,7 @@ async function getAllNotes(maxNotes: number): Promise<Map<string, Note>> {
}

function buildNote(joplinNote: JoplinNote): Note {
const links = getAllLinksForNote(joplinNote.body);
const links: Set<string> = getAllLinksForNote(joplinNote.body);
return {
id: joplinNote.id,
title: joplinNote.title,
Expand Down Expand Up @@ -214,8 +214,8 @@ async function getNoteArray(ids: string[]): Promise<Array<JoplinNote>> {
return valid;
}

function getAllLinksForNote(noteBody: string): Array<string> {
const links = new Array<string>();
export function getAllLinksForNote(noteBody: string): Set<string> {
const links = new Set<string>();
// TODO: needs to handle resource links vs note links. see 4. Tips note for
// webclipper screenshot.
// https://stackoverflow.com/questions/37462126/regex-match-markdown-link
Expand All @@ -224,7 +224,7 @@ function getAllLinksForNote(noteBody: string): Array<string> {
do {
match = linkRegexp.exec(noteBody);
if (match != null && match[1] !== undefined) {
links.push(match[1]);
links.add(match[1]);
}
} while (match != null);
return links;
Expand Down
240 changes: 148 additions & 92 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,29 @@ import joplin from "api";
import * as joplinData from "./data";
import { registerSettings } from "./settings";
import { MenuItemLocation, ToolbarButtonLocation } from "api/types";
var deepEqual = require("deep-equal");
var deepEqual = require("fast-deep-equal");

async function fetchData() {
// Load settings
const maxDegree = await joplin.settings.value(
"SETTING_MAX_SEPARATION_DEGREE"
);
const maxNotes = await joplin.settings.value("SETTING_MAX_NODES");
const filteredNotebookNames = await joplin.settings.value(
"SETTING_NOTEBOOK_NAMES_TO_FILTER"
);
const namesToFilter: Array<string> = filteredNotebookNames.split(",");
const shouldFilterChildren = await joplin.settings.value(
"SETTING_FILTER_CHILD_NOTEBOOKS"
);
const isIncludeFilter =
(await joplin.settings.value("SETTING_FILTER_IS_INCLUDE_FILTER")) ===
"include"
? true
: false;
const isIncludeBacklinks = await joplin.settings.value("SETTING_IS_INCLUDE_BACKLINKS")

const selectedNote = await joplin.workspace.selectedNote();
const notes = await joplinData.getNotes(
selectedNote.id,
maxNotes,
maxDegree,
namesToFilter,
shouldFilterChildren,
isIncludeFilter,
isIncludeBacklinks
);

const data = {
nodes: [],
edges: [],
currentNoteID: selectedNote.id,
nodeFontSize: await joplin.settings.value("SETTING_NODE_FONT_SIZE"),
isIncludeBacklinks: isIncludeBacklinks,
nodeDistanceRatio:
(await joplin.settings.value("SETTING_NODE_DISTANCE")) / 100.0,
};

notes.forEach(function (note, id) {
for (let link of note.links) {
// Slice note link if link directs to an anchor
var index = link.indexOf("#");
if (index != -1) {
link = link.substr(0, index);
}

// The destination note could have been deleted.
const linkDestExists = notes.has(link);
if (!linkDestExists) {
continue;
}

data.edges.push({
source: id,
target: link,
focused: id === selectedNote.id || link === selectedNote.id,
});

// Mark nodes that are adjacent to the currently selected note.
if (id === selectedNote.id) {
notes.get(link).linkedToCurrentNote = true;
} else if (link == selectedNote.id) {
notes.get(id).linkedToCurrentNote = true;
} else {
const l = notes.get(link);
l.linkedToCurrentNote = l.linkedToCurrentNote || false;
}
}
data.nodes.push({
id: id,
title: note.title,
focused: note.linkedToCurrentNote,
});
});

return data;
interface Edge {
source: string;
target: string;
focused: boolean;
}

// rendez-vous between worker and job queue
async function notifyUI() {
if (pollCb && modelChanges.length > 0) {
let modelChange = modelChanges.shift();
pollCb(modelChange);
pollCb = undefined;
}
interface Node {
id: string;
title: string;
focused: boolean;
}

async function recordModelChanges(event) {
modelChanges.push(event);
interface GraphData {
nodes: Node[];
edges: Edge[];
currentNoteID: string;
nodeFontSize: number;
nodeDistanceRatio: number;
}

let data: any;
let data: GraphData;
let pollCb: any;
let modelChanges = [];

Expand All @@ -109,6 +35,7 @@ joplin.plugins.register({
const view = await (panels as any).create("note-graph-view");
await panels.setHtml(view, "Note Graph is Loading");
var prevData = {};
var prevNoteLinks: Set<string>;
var syncOngoing = false;

async function drawPanel() {
Expand Down Expand Up @@ -173,8 +100,44 @@ joplin.plugins.register({
if (syncOngoing) {
return;
}
data = await fetchData();
var dataChanged = !deepEqual(data, prevData);

var dataChanged = false;
// Speed up the inital load by skipping the eventName switch.
if (typeof data === "undefined") {
data = await fetchData();
dataChanged = true;
} else {
switch (eventName) {
case "noteChange":
// Don't update the graph is the links in this note haven't changed.
const selectedNote = await joplin.workspace.selectedNote();
var noteLinks = joplinData.getAllLinksForNote(selectedNote.body);
if (!deepEqual(noteLinks, prevNoteLinks)) {
prevNoteLinks = noteLinks;
dataChanged = true;
}
break;
case "noteSelectionChange":
// noteSelectionChange should just re-center the graph, no need to fetch all new data and compare.
const newlySelectedNote = await joplin.workspace.selectedNote();
data.currentNoteID = newlySelectedNote.id;
data.edges.forEach((edge) => {
const shouldHaveFocus =
edge.source === newlySelectedNote.id ||
edge.target === newlySelectedNote.id;
edge.focused = shouldHaveFocus;
});
data.nodes.forEach((node) => {
node.focused = node.id === newlySelectedNote.id;
});
dataChanged = true;
break;
default:
data = await fetchData();
dataChanged = !deepEqual(data, prevData);
}
}

if (dataChanged) {
prevData = data;
recordModelChanges({ name: eventName, data: data });
Expand All @@ -200,3 +163,96 @@ joplin.plugins.register({
});
},
});

function updateFocus() {}

async function fetchData() {
// Load settings
const maxDegree = await joplin.settings.value(
"SETTING_MAX_SEPARATION_DEGREE"
);
const maxNotes = await joplin.settings.value("SETTING_MAX_NODES");
const filteredNotebookNames = await joplin.settings.value(
"SETTING_NOTEBOOK_NAMES_TO_FILTER"
);
const namesToFilter: Array<string> = filteredNotebookNames.split(",");
const shouldFilterChildren = await joplin.settings.value(
"SETTING_FILTER_CHILD_NOTEBOOKS"
);
const isIncludeFilter =
(await joplin.settings.value("SETTING_FILTER_IS_INCLUDE_FILTER")) ===
"include"
? true
: false;

const selectedNote = await joplin.workspace.selectedNote();
const notes = await joplinData.getNotes(
selectedNote.id,
maxNotes,
maxDegree,
namesToFilter,
shouldFilterChildren,
isIncludeFilter
);

const data: GraphData = {
nodes: [],
edges: [],
currentNoteID: selectedNote.id,
nodeFontSize: await joplin.settings.value("SETTING_NODE_FONT_SIZE"),
nodeDistanceRatio:
(await joplin.settings.value("SETTING_NODE_DISTANCE")) / 100.0,
};

notes.forEach(function (note, id) {
for (let link of note.links) {
// Slice note link if link directs to an anchor
var index = link.indexOf("#");
if (index != -1) {
link = link.substr(0, index);
}

// The destination note could have been deleted.
const linkDestExists = notes.has(link);
if (!linkDestExists) {
continue;
}

data.edges.push({
source: id,
target: link,
focused: id === selectedNote.id || link === selectedNote.id,
});

// Mark nodes that are adjacent to the currently selected note.
if (id === selectedNote.id) {
notes.get(link).linkedToCurrentNote = true;
} else if (link == selectedNote.id) {
notes.get(id).linkedToCurrentNote = true;
} else {
const l = notes.get(link);
l.linkedToCurrentNote = l.linkedToCurrentNote || false;
}
}
data.nodes.push({
id: id,
title: note.title,
focused: note.linkedToCurrentNote,
});
});

return data;
}

// rendez-vous between worker and job queue
async function notifyUI() {
if (pollCb && modelChanges.length > 0) {
let modelChange = modelChanges.shift();
pollCb(modelChange);
pollCb = undefined;
}
}

async function recordModelChanges(event) {
modelChanges.push(event);
}