From d5aee2ea58a16e0cf65213fab9e46192882feba9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 17 Nov 2022 09:31:07 +0900
Subject: [PATCH] improve performance

---
 packages/backend/src/core/RelayService.ts     |  3 +-
 .../core/entities/DriveFileEntityService.ts   |  3 +-
 packages/backend/src/misc/clone.ts            | 18 ++++++++
 .../src/server/web/ClientServerService.ts     |  3 +-
 packages/client/src/components/MkNote.vue     |  5 ++-
 .../client/src/components/MkNoteDetailed.vue  |  5 ++-
 packages/client/src/components/MkPostForm.vue |  3 +-
 .../client/src/pages/settings/reaction.vue    |  5 ++-
 .../pages/settings/statusbar.statusbar.vue    |  7 +--
 packages/client/src/scripts/clone.ts          | 18 ++++++++
 packages/client/src/scripts/theme.ts          |  3 +-
 packages/client/src/ui/deck/deck-store.ts     | 45 +++++++++----------
 packages/client/src/widgets/job-queue.vue     |  7 +--
 packages/client/src/widgets/widget.ts         |  7 +--
 14 files changed, 88 insertions(+), 44 deletions(-)
 create mode 100644 packages/backend/src/misc/clone.ts
 create mode 100644 packages/client/src/scripts/clone.ts

diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index 563eeac0f0..3c67e0573f 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -9,6 +9,7 @@ import { QueueService } from '@/core/QueueService.js';
 import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
 import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js';
 import { DI } from '@/di-symbols.js';
+import { deepClone } from '@/misc/clone.js';
 
 const ACTOR_USERNAME = 'relay.actor' as const;
 
@@ -105,7 +106,7 @@ export class RelayService {
 		}));
 		if (relays.length === 0) return;
 	
-		const copy = structuredClone(activity);
+		const copy = deepClone(activity);
 		if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
 	
 		const signed = await this.apRendererService.attachLdSignature(copy, user);
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index d9430e1497..e0aeb70dfc 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -9,6 +9,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
 import type { User } from '@/models/entities/User.js';
 import type { DriveFile } from '@/models/entities/DriveFile.js';
 import { appendQuery, query } from '@/misc/prelude/url.js';
+import { deepClone } from '@/misc/clone.js';
 import { UtilityService } from '../UtilityService.js';
 import { UserEntityService } from './UserEntityService.js';
 import { DriveFolderEntityService } from './DriveFolderEntityService.js';
@@ -55,7 +56,7 @@ export class DriveFileEntityService {
 
 	public getPublicProperties(file: DriveFile): DriveFile['properties'] {
 		if (file.properties.orientation != null) {
-			const properties = structuredClone(file.properties);
+			const properties = deepClone(file.properties);
 			if (file.properties.orientation >= 5) {
 				[properties.width, properties.height] = [properties.height, properties.width];
 			}
diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts
new file mode 100644
index 0000000000..16fad24129
--- /dev/null
+++ b/packages/backend/src/misc/clone.ts
@@ -0,0 +1,18 @@
+// structredCloneが遅いため
+// SEE: http://var.blog.jp/archives/86038606.html
+
+type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
+
+export function deepClone<T extends Cloneable>(x: T): T {
+	if (typeof x === 'object') {
+		if (x === null) return x;
+		if (Array.isArray(x)) return x.map(deepClone) as T;
+		const obj = {} as Record<string, Cloneable>;
+		for (const [k, v] of Object.entries(x)) {
+			obj[k] = deepClone(v);
+		}
+		return obj as T;
+	} else {
+		return x;
+	}
+}
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 44450245a6..8957a91309 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -26,6 +26,7 @@ import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityServi
 import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
 import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
 import type { ChannelsRepository, ClipsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
+import { deepClone } from '@/misc/clone.js';
 import manifest from './manifest.json' assert { type: 'json' };
 import { FeedService } from './FeedService.js';
 import { UrlPreviewService } from './UrlPreviewService.js';
@@ -86,7 +87,7 @@ export class ClientServerService {
 	}
 
 	private async manifestHandler(ctx: Koa.Context) {
-		const res = structuredClone(manifest);
+		const res = deepClone(manifest);
 
 		const instance = await this.metaService.fetch(true);
 
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index efe786ba4b..97eadb1945 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -129,6 +129,7 @@ import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { getNoteMenu } from '@/scripts/get-note-menu';
 import { useNoteCapture } from '@/scripts/use-note-capture';
+import { deepClone } from '@/scripts/clone';
 
 const props = defineProps<{
 	note: misskey.entities.Note;
@@ -137,12 +138,12 @@ const props = defineProps<{
 
 const inChannel = inject('inChannel', null);
 
-let note = $ref(JSON.parse(JSON.stringify(props.note)));
+let note = $ref(deepClone(props.note));
 
 // plugin
 if (noteViewInterruptors.length > 0) {
 	onMounted(async () => {
-		let result = JSON.parse(JSON.stringify(note));
+		let result = deepClone(note);
 		for (const interruptor of noteViewInterruptors) {
 			result = await interruptor.handler(result);
 		}
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index 0bf8f330ba..82468027fd 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -139,6 +139,7 @@ import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { getNoteMenu } from '@/scripts/get-note-menu';
 import { useNoteCapture } from '@/scripts/use-note-capture';
+import { deepClone } from '@/scripts/clone';
 
 const props = defineProps<{
 	note: misskey.entities.Note;
@@ -147,12 +148,12 @@ const props = defineProps<{
 
 const inChannel = inject('inChannel', null);
 
-let note = $ref(JSON.parse(JSON.stringify(props.note)));
+let note = $ref(deepClone(props.note));
 
 // plugin
 if (noteViewInterruptors.length > 0) {
 	onMounted(async () => {
-		let result = JSON.parse(JSON.stringify(note));
+		let result = deepClone(note);
 		for (const interruptor of noteViewInterruptors) {
 			result = await interruptor.handler(result);
 		}
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index 0c57a5a57a..24f2bfb9e6 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -89,6 +89,7 @@ import { i18n } from '@/i18n';
 import { instance } from '@/instance';
 import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
 import { uploadFile } from '@/scripts/upload';
+import { deepClone } from '@/scripts/clone';
 
 const modal = inject('modal');
 
@@ -575,7 +576,7 @@ async function post() {
 	// plugin
 	if (notePostInterruptors.length > 0) {
 		for (const interruptor of notePostInterruptors) {
-			postData = await interruptor.handler(JSON.parse(JSON.stringify(postData)));
+			postData = await interruptor.handler(deepClone(postData));
 		}
 	}
 
diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
index c23c1c2375..f8d57cbcd5 100644
--- a/packages/client/src/pages/settings/reaction.vue
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -66,8 +66,9 @@ import * as os from '@/os';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { deepClone } from '@/scripts/clone';
 
-let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions)));
+let reactions = $ref(deepClone(defaultStore.state.reactions));
 
 const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
 const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
@@ -101,7 +102,7 @@ async function setDefault() {
 	});
 	if (canceled) return;
 
-	reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
+	reactions = deepClone(defaultStore.def.reactions.default);
 }
 
 function chooseEmoji(ev: MouseEvent) {
diff --git a/packages/client/src/pages/settings/statusbar.statusbar.vue b/packages/client/src/pages/settings/statusbar.statusbar.vue
index 98a1825b95..608222386e 100644
--- a/packages/client/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/client/src/pages/settings/statusbar.statusbar.vue
@@ -91,13 +91,14 @@ import FormRange from '@/components/form/range.vue';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
+import { deepClone } from '@/scripts/clone';
 
 const props = defineProps<{
 	_id: string;
 	userLists: any[] | null;
 }>();
 
-const statusbar = reactive(JSON.parse(JSON.stringify(defaultStore.state.statusbars.find(x => x.id === props._id))));
+const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
 
 watch(() => statusbar.type, () => {
 	if (statusbar.type === 'rss') {
@@ -128,8 +129,8 @@ watch(statusbar, save);
 
 async function save() {
 	const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
-	const statusbars = JSON.parse(JSON.stringify(defaultStore.state.statusbars));
-	statusbars[i] = JSON.parse(JSON.stringify(statusbar));
+	const statusbars = deepClone(defaultStore.state.statusbars);
+	statusbars[i] = deepClone(statusbar);
 	defaultStore.set('statusbars', statusbars);
 }
 
diff --git a/packages/client/src/scripts/clone.ts b/packages/client/src/scripts/clone.ts
new file mode 100644
index 0000000000..16fad24129
--- /dev/null
+++ b/packages/client/src/scripts/clone.ts
@@ -0,0 +1,18 @@
+// structredCloneが遅いため
+// SEE: http://var.blog.jp/archives/86038606.html
+
+type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
+
+export function deepClone<T extends Cloneable>(x: T): T {
+	if (typeof x === 'object') {
+		if (x === null) return x;
+		if (Array.isArray(x)) return x.map(deepClone) as T;
+		const obj = {} as Record<string, Cloneable>;
+		for (const [k, v] of Object.entries(x)) {
+			obj[k] = deepClone(v);
+		}
+		return obj as T;
+	} else {
+		return x;
+	}
+}
diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts
index 3f55d9ae86..62a2b9459a 100644
--- a/packages/client/src/scripts/theme.ts
+++ b/packages/client/src/scripts/theme.ts
@@ -13,6 +13,7 @@ export type Theme = {
 
 import lightTheme from '@/themes/_light.json5';
 import darkTheme from '@/themes/_dark.json5';
+import { deepClone } from './clone';
 
 export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
 
@@ -60,7 +61,7 @@ export function applyTheme(theme: Theme, persist = true) {
 	const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
 
 	// Deep copy
-	const _theme = JSON.parse(JSON.stringify(theme));
+	const _theme = deepClone(theme);
 
 	if (_theme.base) {
 		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts
index 67fcff4807..56db7398e5 100644
--- a/packages/client/src/ui/deck/deck-store.ts
+++ b/packages/client/src/ui/deck/deck-store.ts
@@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
 import { Storage } from '../../pizzax';
 import { i18n } from '@/i18n';
 import { api } from '@/os';
+import { deepClone } from '@/scripts/clone';
 
 type ColumnWidget = {
 	name: string;
@@ -25,10 +26,6 @@ export type Column = {
 	tl?: 'home' | 'local' | 'social' | 'global';
 };
 
-function copy<T>(x: T): T {
-	return JSON.parse(JSON.stringify(x));
-}
-
 export const deckStore = markRaw(new Storage('deck', {
 	profile: {
 		where: 'deviceAccount',
@@ -128,7 +125,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
 	const aY = deckStore.state.layout[aX].findIndex(id => id === a);
 	const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
 	const bY = deckStore.state.layout[bX].findIndex(id => id === b);
-	const layout = copy(deckStore.state.layout);
+	const layout = deepClone(deckStore.state.layout);
 	layout[aX][aY] = b;
 	layout[bX][bY] = a;
 	deckStore.set('layout', layout);
@@ -136,7 +133,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
 }
 
 export function swapLeftColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
+	const layout = deepClone(deckStore.state.layout);
 	deckStore.state.layout.some((ids, i) => {
 		if (ids.includes(id)) {
 			const left = deckStore.state.layout[i - 1];
@@ -152,7 +149,7 @@ export function swapLeftColumn(id: Column['id']) {
 }
 
 export function swapRightColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
+	const layout = deepClone(deckStore.state.layout);
 	deckStore.state.layout.some((ids, i) => {
 		if (ids.includes(id)) {
 			const right = deckStore.state.layout[i + 1];
@@ -168,9 +165,9 @@ export function swapRightColumn(id: Column['id']) {
 }
 
 export function swapUpColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
+	const layout = deepClone(deckStore.state.layout);
 	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = copy(deckStore.state.layout[idsIndex]);
+	const ids = deepClone(deckStore.state.layout[idsIndex]);
 	ids.some((x, i) => {
 		if (x === id) {
 			const up = ids[i - 1];
@@ -188,9 +185,9 @@ export function swapUpColumn(id: Column['id']) {
 }
 
 export function swapDownColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
+	const layout = deepClone(deckStore.state.layout);
 	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = copy(deckStore.state.layout[idsIndex]);
+	const ids = deepClone(deckStore.state.layout[idsIndex]);
 	ids.some((x, i) => {
 		if (x === id) {
 			const down = ids[i + 1];
@@ -208,7 +205,7 @@ export function swapDownColumn(id: Column['id']) {
 }
 
 export function stackLeftColumn(id: Column['id']) {
-	let layout = copy(deckStore.state.layout);
+	let layout = deepClone(deckStore.state.layout);
 	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
 	layout = layout.map(ids => ids.filter(_id => _id !== id));
 	layout[i - 1].push(id);
@@ -218,7 +215,7 @@ export function stackLeftColumn(id: Column['id']) {
 }
 
 export function popRightColumn(id: Column['id']) {
-	let layout = copy(deckStore.state.layout);
+	let layout = deepClone(deckStore.state.layout);
 	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
 	const affected = layout[i];
 	layout = layout.map(ids => ids.filter(_id => _id !== id));
@@ -226,7 +223,7 @@ export function popRightColumn(id: Column['id']) {
 	layout = layout.filter(ids => ids.length > 0);
 	deckStore.set('layout', layout);
 
-	const columns = copy(deckStore.state.columns);
+	const columns = deepClone(deckStore.state.columns);
 	for (const column of columns) {
 		if (affected.includes(column.id)) {
 			column.active = true;
@@ -238,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
 }
 
 export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = copy(deckStore.state.columns);
+	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
+	const column = deepClone(deckStore.state.columns[columnIndex]);
 	if (column == null) return;
 	if (column.widgets == null) column.widgets = [];
 	column.widgets.unshift(widget);
@@ -250,9 +247,9 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
 }
 
 export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = copy(deckStore.state.columns);
+	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
+	const column = deepClone(deckStore.state.columns[columnIndex]);
 	if (column == null) return;
 	column.widgets = column.widgets.filter(w => w.id !== widget.id);
 	columns[columnIndex] = column;
@@ -261,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
 }
 
 export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
-	const columns = copy(deckStore.state.columns);
+	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
+	const column = deepClone(deckStore.state.columns[columnIndex]);
 	if (column == null) return;
 	column.widgets = widgets;
 	columns[columnIndex] = column;
@@ -272,9 +269,9 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
 }
 
 export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
-	const columns = copy(deckStore.state.columns);
+	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
+	const column = deepClone(deckStore.state.columns[columnIndex]);
 	if (column == null) return;
 	column.widgets = column.widgets.map(w => w.id === widgetId ? {
 		...w,
@@ -286,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
 }
 
 export function updateColumn(id: Column['id'], column: Partial<Column>) {
-	const columns = copy(deckStore.state.columns);
+	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const currentColumn = copy(deckStore.state.columns[columnIndex]);
+	const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
 	if (currentColumn == null) return;
 	for (const [k, v] of Object.entries(column)) {
 		currentColumn[k] = v;
diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue
index 8897f240bd..363d1b3ea0 100644
--- a/packages/client/src/widgets/job-queue.vue
+++ b/packages/client/src/widgets/job-queue.vue
@@ -47,12 +47,13 @@
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, reactive, ref } from 'vue';
-import { GetFormResultType } from '@/scripts/form';
 import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { GetFormResultType } from '@/scripts/form';
 import { stream } from '@/stream';
 import number from '@/filters/number';
 import * as sound from '@/scripts/sound';
 import * as os from '@/os';
+import { deepClone } from '@/scripts/clone';
 
 const name = 'jobQueue';
 
@@ -100,12 +101,12 @@ const prev = reactive({} as typeof current);
 const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
 
 for (const domain of ['inbox', 'deliver']) {
-	prev[domain] = JSON.parse(JSON.stringify(current[domain]));
+	prev[domain] = deepClone(current[domain]);
 }
 
 const onStats = (stats) => {
 	for (const domain of ['inbox', 'deliver']) {
-		prev[domain] = JSON.parse(JSON.stringify(current[domain]));
+		prev[domain] = deepClone(current[domain]);
 		current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
 		current[domain].active = stats[domain].active;
 		current[domain].waiting = stats[domain].waiting;
diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts
index 9fdfe7f3e1..8bd56a5966 100644
--- a/packages/client/src/widgets/widget.ts
+++ b/packages/client/src/widgets/widget.ts
@@ -2,6 +2,7 @@ import { reactive, watch } from 'vue';
 import { throttle } from 'throttle-debounce';
 import { Form, GetFormResultType } from '@/scripts/form';
 import * as os from '@/os';
+import { deepClone } from '@/scripts/clone';
 
 export type Widget<P extends Record<string, unknown>> = {
 	id: string;
@@ -32,7 +33,7 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
 	save: () => void;
 	configure: () => void;
 } => {
-	const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {});
+	const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {});
 
 	const mergeProps = () => {
 		for (const prop of Object.keys(propsDef)) {
@@ -43,14 +44,14 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
 	};
 	watch(widgetProps, () => {
 		mergeProps();
-	}, { deep: true, immediate: true, });
+	}, { deep: true, immediate: true });
 
 	const save = throttle(3000, () => {
 		emit('updateProps', widgetProps);
 	});
 
 	const configure = async () => {
-		const form = JSON.parse(JSON.stringify(propsDef));
+		const form = deepClone(propsDef);
 		for (const item of Object.keys(form)) {
 			form[item].default = widgetProps[item];
 		}
-- 
GitLab