From 8aa089178a54559cbc4e4fe84a618fc7535f178c Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 10:49:14 +0900 Subject: Improve server performance --- src/misc/cache.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/misc/cache.ts (limited to 'src/misc') diff --git a/src/misc/cache.ts b/src/misc/cache.ts new file mode 100644 index 0000000000..356a3de7b9 --- /dev/null +++ b/src/misc/cache.ts @@ -0,0 +1,25 @@ +export class Cache { + private cache: Map; + private lifetime: number; + + constructor(lifetime: Cache['lifetime']) { + this.lifetime = lifetime; + } + + public set(key: string | null, value: T):void { + this.cache.set(key, { + date: Date.now(), + value + }); + } + + public get(key: string | null): T | null { + const cached = this.cache.get(key); + if (cached == null) return null; + if ((Date.now() - cached.date) > this.lifetime) { + this.cache.delete(key); + return null; + } + return cached.value; + } +} -- cgit v1.2.3-freya From 0389e2c891177db3c2e6dfbc0b9656dfddf90892 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 10:54:39 +0900 Subject: fix bug --- src/misc/cache.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src/misc') diff --git a/src/misc/cache.ts b/src/misc/cache.ts index 356a3de7b9..ccc5f01ff7 100644 --- a/src/misc/cache.ts +++ b/src/misc/cache.ts @@ -3,6 +3,7 @@ export class Cache { private lifetime: number; constructor(lifetime: Cache['lifetime']) { + this.cache = new Map(); this.lifetime = lifetime; } -- cgit v1.2.3-freya From e53a40658d40a91bfecc005ef1be712bbddbf336 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 10:55:51 +0900 Subject: format --- src/misc/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/misc') diff --git a/src/misc/cache.ts b/src/misc/cache.ts index ccc5f01ff7..5b7017a3b9 100644 --- a/src/misc/cache.ts +++ b/src/misc/cache.ts @@ -7,7 +7,7 @@ export class Cache { this.lifetime = lifetime; } - public set(key: string | null, value: T):void { + public set(key: string | null, value: T): void { this.cache.set(key, { date: Date.now(), value -- cgit v1.2.3-freya From 4f249159d310d4296753fcfe8e2bbd390fd9002b Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 11:17:05 +0900 Subject: Improve chart performance (#7360) * wip * wip * wip * wip * wip * Update chart.ts * wip * Improve server performance * wip * wip --- migration/1615965918224-chart-v2.ts | 218 ++++++++++++++++ migration/1615966519402-chart-v2-2.ts | 22 ++ package.json | 2 +- src/daemons/queue-stats.ts | 2 +- src/db/postgre.ts | 4 + src/global.d.ts | 1 + src/misc/before-shutdown.ts | 88 +++++++ src/queue/index.ts | 23 +- src/queue/initialize.ts | 18 ++ src/queue/queues.ts | 7 + src/services/chart/charts/classes/active-users.ts | 18 +- src/services/chart/charts/classes/drive.ts | 22 ++ src/services/chart/charts/classes/federation.ts | 11 + src/services/chart/charts/classes/hashtag.ts | 18 +- src/services/chart/charts/classes/instance.ts | 44 ++++ src/services/chart/charts/classes/network.ts | 11 + src/services/chart/charts/classes/notes.ts | 26 ++ .../chart/charts/classes/per-user-drive.ts | 12 + .../chart/charts/classes/per-user-following.ts | 30 +++ .../chart/charts/classes/per-user-notes.ts | 14 + .../chart/charts/classes/per-user-reactions.ts | 12 + src/services/chart/charts/classes/test-grouped.ts | 11 + src/services/chart/charts/classes/test-unique.ts | 13 +- src/services/chart/charts/classes/test.ts | 11 + src/services/chart/charts/classes/users.ts | 16 ++ src/services/chart/charts/schemas/active-users.ts | 12 +- src/services/chart/charts/schemas/hashtag.ts | 12 +- src/services/chart/charts/schemas/test-unique.ts | 7 +- src/services/chart/core.ts | 282 ++++++++++++--------- src/services/chart/index.ts | 25 ++ test/chart.ts | 54 +++- yarn.lock | 8 +- 32 files changed, 891 insertions(+), 163 deletions(-) create mode 100644 migration/1615965918224-chart-v2.ts create mode 100644 migration/1615966519402-chart-v2-2.ts create mode 100644 src/global.d.ts create mode 100644 src/misc/before-shutdown.ts create mode 100644 src/queue/initialize.ts create mode 100644 src/queue/queues.ts (limited to 'src/misc') diff --git a/migration/1615965918224-chart-v2.ts b/migration/1615965918224-chart-v2.ts new file mode 100644 index 0000000000..cacbd1945b --- /dev/null +++ b/migration/1615965918224-chart-v2.ts @@ -0,0 +1,218 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class chartV21615965918224 implements MigrationInterface { + name = 'chartV21615965918224' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM "__chart__active_users" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__drive" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__federation" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__hashtag" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__instance" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__network" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__notes" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__per_user_drive" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__per_user_following" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__per_user_notes" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__test" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__test_grouped" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__test_unique" WHERE "span" = 'day'`); + await queryRunner.query(`DELETE FROM "__chart__users" WHERE "span" = 'day'`); + + await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`); + await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`); + await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`); + await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`); + await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`); + await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`); + await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`); + await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`); + await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`); + await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`); + await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`); + await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`); + await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`); + await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`); + await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`); + await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`); + await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`); + await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`); + await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`); + await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`); + await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`); + await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`); + await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`); + await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`); + await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`); + await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`); + await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`); + await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`); + await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`); + await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`); + await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`); + await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`); + await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`); + await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`); + await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`); + await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`); + await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`); + await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`); + await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`); + await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`); + await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`); + await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`); + await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`); + await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`); + await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__active_users_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_count"`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_count"`); + await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__drive_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__federation_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__hashtag_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_count"`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_count"`); + await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__instance_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__network_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__notes_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__per_user_drive_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__per_user_following_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__per_user_notes_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__per_user_reaction_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__test_grouped_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__test_unique_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`); + await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__test_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "unique"`); + await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "span"`); + await queryRunner.query(`DROP TYPE "public"."__chart__users_span_enum"`); + await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "unique"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "__chart__users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__users_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__users" ADD "span" "__chart__users_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__test" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__test_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__test" ADD "span" "__chart__test_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" bigint NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__test_unique_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "span" "__chart__test_unique_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "span" "__chart__test_grouped_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "span" "__chart__per_user_reaction_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "span" "__chart__per_user_notes_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "span" "__chart__per_user_following_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "span" "__chart__per_user_drive_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__notes_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "span" "__chart__notes_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__network" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__network_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__network" ADD "span" "__chart__network_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__instance_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "span" "__chart__instance_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_count" bigint NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_count" bigint NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__hashtag_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "span" "__chart__hashtag_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__federation_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "span" "__chart__federation_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__drive_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "span" "__chart__drive_span_enum" NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_count" bigint NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_count" bigint NOT NULL`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); + await queryRunner.query(`CREATE TYPE "public"."__chart__active_users_span_enum" AS ENUM('hour', 'day')`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "span" "__chart__active_users_span_enum" NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `); + await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("date", "group", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("date", "span") `); + await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `); + } + +} diff --git a/migration/1615966519402-chart-v2-2.ts b/migration/1615966519402-chart-v2-2.ts new file mode 100644 index 0000000000..a694f9542a --- /dev/null +++ b/migration/1615966519402-chart-v2-2.ts @@ -0,0 +1,22 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class chartV221615966519402 implements MigrationInterface { + name = 'chartV221615966519402' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); + await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" character varying array NOT NULL DEFAULT '{}'::varchar[]`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_users"`); + await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_users"`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`); + } + +} diff --git a/package.json b/package.json index 67d7dfca89..4d7f3c8353 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@koa/router": "9.0.1", "@sentry/browser": "5.29.2", "@sentry/tracing": "5.29.2", - "@sinonjs/fake-timers": "6.0.1", + "@sinonjs/fake-timers": "7.0.2", "@syuilo/aiscript": "0.11.1", "@types/bcryptjs": "2.4.2", "@types/bull": "3.15.0", diff --git a/src/daemons/queue-stats.ts b/src/daemons/queue-stats.ts index 288e855ae9..77f09b18d6 100644 --- a/src/daemons/queue-stats.ts +++ b/src/daemons/queue-stats.ts @@ -1,5 +1,5 @@ import Xev from 'xev'; -import { deliverQueue, inboxQueue } from '../queue'; +import { deliverQueue, inboxQueue } from '../queue/queues'; const ev = new Xev(); diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 2f3c910163..831e5e0592 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -1,3 +1,7 @@ +// https://github.com/typeorm/typeorm/issues/2400 +const types = require('pg').types; +types.setTypeParser(20, Number); + import { createConnection, Logger, getConnection } from 'typeorm'; import config from '../config'; import { entities as charts } from '../services/chart/entities'; diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000000..7343aa1994 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +type FIXME = any; diff --git a/src/misc/before-shutdown.ts b/src/misc/before-shutdown.ts new file mode 100644 index 0000000000..58d0ea5108 --- /dev/null +++ b/src/misc/before-shutdown.ts @@ -0,0 +1,88 @@ +// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58 + +'use strict'; + +/** + * @callback BeforeShutdownListener + * @param {string} [signalOrEvent] The exit signal or event name received on the process. + */ + +/** + * System signals the app will listen to initiate shutdown. + * @const {string[]} + */ +const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM']; + +/** + * Time in milliseconds to wait before forcing shutdown. + * @const {number} + */ +const SHUTDOWN_TIMEOUT = 15000; + +/** + * A queue of listener callbacks to execute before shutting + * down the process. + * @type {BeforeShutdownListener[]} + */ +const shutdownListeners = []; + +/** + * Listen for signals and execute given `fn` function once. + * @param {string[]} signals System signals to listen to. + * @param {function(string)} fn Function to execute on shutdown. + */ +const processOnce = (signals, fn) => { + return signals.forEach(sig => process.once(sig, fn)); +}; + +/** + * Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds. + * @param {number} timeout Time to wait before forcing shutdown (milliseconds) + */ +const forceExitAfter = timeout => () => { + setTimeout(() => { + // Force shutdown after timeout + console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`); + return process.exit(1); + }, timeout).unref(); +}; + +/** + * Main process shutdown handler. Will invoke every previously registered async shutdown listener + * in the queue and exit with a code of `0`. Any `Promise` rejections from any listener will + * be logged out as a warning, but won't prevent other callbacks from executing. + * @param {string} signalOrEvent The exit signal or event name received on the process. + */ +async function shutdownHandler(signalOrEvent) { + console.warn(`Shutting down: received [${signalOrEvent}] signal`); + + for (const listener of shutdownListeners) { + try { + await listener(signalOrEvent); + } catch (err) { + console.warn(`A shutdown handler failed before completing with: ${err.message || err}`); + } + } + + return process.exit(0); +} + +/** + * Registers a new shutdown listener to be invoked before exiting + * the main process. Listener handlers are guaranteed to be called in the order + * they were registered. + * @param {BeforeShutdownListener} listener The shutdown listener to register. + * @returns {BeforeShutdownListener} Echoes back the supplied `listener`. + */ +export function beforeShutdown(listener) { + shutdownListeners.push(listener); + return listener; +} + +// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds +// This prevents custom shutdown handlers from hanging the process indefinitely +processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT)); + +// Register process shutdown callback +// Will listen to incoming signal events and execute all registered handlers in the stack +processOnce(SHUTDOWN_SIGNALS, shutdownHandler); diff --git a/src/queue/index.ts b/src/queue/index.ts index 163c57d691..9fb4595a35 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -1,4 +1,3 @@ -import * as Queue from 'bull'; import * as httpSignature from 'http-signature'; import config from '../config'; @@ -13,22 +12,7 @@ import { queueLogger } from './logger'; import { DriveFile } from '../models/entities/drive-file'; import { getJobInfo } from './get-job-info'; import { IActivity } from '../remote/activitypub/type'; - -function initializeQueue(name: string, limitPerSec = -1) { - return new Queue(name, { - redis: { - port: config.redis.port, - host: config.redis.host, - password: config.redis.pass, - db: config.redis.db || 0, - }, - prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue', - limiter: limitPerSec > 0 ? { - max: limitPerSec * 5, - duration: 5000 - } : undefined - }); -} +import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues'; export type InboxJobData = { activity: IActivity, @@ -44,11 +28,6 @@ function renderError(e: Error): any { }; } -export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec || 128); -export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); -export const dbQueue = initializeQueue('db'); -export const objectStorageQueue = initializeQueue('objectStorage'); - const deliverLogger = queueLogger.createSubLogger('deliver'); const inboxLogger = queueLogger.createSubLogger('inbox'); const dbLogger = queueLogger.createSubLogger('db'); diff --git a/src/queue/initialize.ts b/src/queue/initialize.ts new file mode 100644 index 0000000000..92579531e4 --- /dev/null +++ b/src/queue/initialize.ts @@ -0,0 +1,18 @@ +import * as Queue from 'bull'; +import config from '../config'; + +export function initialize(name: string, limitPerSec = -1) { + return new Queue(name, { + redis: { + port: config.redis.port, + host: config.redis.host, + password: config.redis.pass, + db: config.redis.db || 0, + }, + prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue', + limiter: limitPerSec > 0 ? { + max: limitPerSec * 5, + duration: 5000 + } : undefined + }); +} diff --git a/src/queue/queues.ts b/src/queue/queues.ts new file mode 100644 index 0000000000..d589d9f7da --- /dev/null +++ b/src/queue/queues.ts @@ -0,0 +1,7 @@ +import config from '../config'; +import { initialize as initializeQueue } from './initialize'; + +export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec || 128); +export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); +export const dbQueue = initializeQueue('db'); +export const objectStorageQueue = initializeQueue('objectStorage'); diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts index 5128150de6..4820f8281b 100644 --- a/src/services/chart/charts/classes/active-users.ts +++ b/src/services/chart/charts/classes/active-users.ts @@ -17,6 +17,18 @@ export default class ActiveUsersChart extends Chart { return {}; } + @autobind + protected aggregate(logs: ActiveUsersLog[]): ActiveUsersLog { + return { + local: { + users: logs.reduce((a, b) => a.concat(b.local.users), [] as ActiveUsersLog['local']['users']), + }, + remote: { + users: logs.reduce((a, b) => a.concat(b.remote.users), [] as ActiveUsersLog['remote']['users']), + }, + }; + } + @autobind protected async fetchActual(): Promise> { return {}; @@ -25,11 +37,11 @@ export default class ActiveUsersChart extends Chart { @autobind public async update(user: User) { const update: Obj = { - count: 1 + users: [user.id] }; - await this.incIfUnique({ + await this.inc({ [Users.isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user.id); + }); } } diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts index 57bb120beb..46399a34d8 100644 --- a/src/services/chart/charts/classes/drive.ts +++ b/src/services/chart/charts/classes/drive.ts @@ -27,6 +27,28 @@ export default class DriveChart extends Chart { }; } + @autobind + protected aggregate(logs: DriveLog[]): DriveLog { + return { + local: { + totalCount: logs[0].local.totalCount, + totalSize: logs[0].local.totalSize, + incCount: logs.reduce((a, b) => a + b.local.incCount, 0), + incSize: logs.reduce((a, b) => a + b.local.incSize, 0), + decCount: logs.reduce((a, b) => a + b.local.decCount, 0), + decSize: logs.reduce((a, b) => a + b.local.decSize, 0), + }, + remote: { + totalCount: logs[0].remote.totalCount, + totalSize: logs[0].remote.totalSize, + incCount: logs.reduce((a, b) => a + b.remote.incCount, 0), + incSize: logs.reduce((a, b) => a + b.remote.incSize, 0), + decCount: logs.reduce((a, b) => a + b.remote.decCount, 0), + decSize: logs.reduce((a, b) => a + b.remote.decSize, 0), + }, + }; + } + @autobind protected async fetchActual(): Promise> { const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([ diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts index bd2c497e7b..ab6ec2d4dd 100644 --- a/src/services/chart/charts/classes/federation.ts +++ b/src/services/chart/charts/classes/federation.ts @@ -20,6 +20,17 @@ export default class FederationChart extends Chart { }; } + @autobind + protected aggregate(logs: FederationLog[]): FederationLog { + return { + instance: { + total: logs[0].instance.total, + inc: logs.reduce((a, b) => a + b.instance.inc, 0), + dec: logs.reduce((a, b) => a + b.instance.dec, 0), + }, + }; + } + @autobind protected async fetchActual(): Promise> { const [total] = await Promise.all([ diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts index 38c3a94f0c..43db5b0a83 100644 --- a/src/services/chart/charts/classes/hashtag.ts +++ b/src/services/chart/charts/classes/hashtag.ts @@ -17,6 +17,18 @@ export default class HashtagChart extends Chart { return {}; } + @autobind + protected aggregate(logs: HashtagLog[]): HashtagLog { + return { + local: { + users: logs.reduce((a, b) => a.concat(b.local.users), [] as HashtagLog['local']['users']), + }, + remote: { + users: logs.reduce((a, b) => a.concat(b.remote.users), [] as HashtagLog['remote']['users']), + }, + }; + } + @autobind protected async fetchActual(): Promise> { return {}; @@ -25,11 +37,11 @@ export default class HashtagChart extends Chart { @autobind public async update(hashtag: string, user: User) { const update: Obj = { - count: 1 + users: [user.id] }; - await this.incIfUnique({ + await this.inc({ [Users.isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user.id, hashtag); + }, hashtag); } } diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts index 7575abfb6f..c32b864d87 100644 --- a/src/services/chart/charts/classes/instance.ts +++ b/src/services/chart/charts/classes/instance.ts @@ -36,6 +36,50 @@ export default class InstanceChart extends Chart { }; } + @autobind + protected aggregate(logs: InstanceLog[]): InstanceLog { + return { + requests: { + failed: logs.reduce((a, b) => a + b.requests.failed, 0), + succeeded: logs.reduce((a, b) => a + b.requests.succeeded, 0), + received: logs.reduce((a, b) => a + b.requests.received, 0), + }, + notes: { + total: logs[0].notes.total, + inc: logs.reduce((a, b) => a + b.notes.inc, 0), + dec: logs.reduce((a, b) => a + b.notes.dec, 0), + diffs: { + reply: logs.reduce((a, b) => a + b.notes.diffs.reply, 0), + renote: logs.reduce((a, b) => a + b.notes.diffs.renote, 0), + normal: logs.reduce((a, b) => a + b.notes.diffs.normal, 0), + }, + }, + users: { + total: logs[0].users.total, + inc: logs.reduce((a, b) => a + b.users.inc, 0), + dec: logs.reduce((a, b) => a + b.users.dec, 0), + }, + following: { + total: logs[0].following.total, + inc: logs.reduce((a, b) => a + b.following.inc, 0), + dec: logs.reduce((a, b) => a + b.following.dec, 0), + }, + followers: { + total: logs[0].followers.total, + inc: logs.reduce((a, b) => a + b.followers.inc, 0), + dec: logs.reduce((a, b) => a + b.followers.dec, 0), + }, + drive: { + totalFiles: logs[0].drive.totalFiles, + totalUsage: logs[0].drive.totalUsage, + incFiles: logs.reduce((a, b) => a + b.drive.incFiles, 0), + incUsage: logs.reduce((a, b) => a + b.drive.incUsage, 0), + decFiles: logs.reduce((a, b) => a + b.drive.decFiles, 0), + decUsage: logs.reduce((a, b) => a + b.drive.decUsage, 0), + }, + }; + } + @autobind protected async fetchActual(group: string): Promise> { const [ diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts index 8b26e5c4c2..693af48f73 100644 --- a/src/services/chart/charts/classes/network.ts +++ b/src/services/chart/charts/classes/network.ts @@ -15,6 +15,17 @@ export default class NetworkChart extends Chart { return {}; } + @autobind + protected aggregate(logs: NetworkLog[]): NetworkLog { + return { + incomingRequests: logs.reduce((a, b) => a + b.incomingRequests, 0), + outgoingRequests: logs.reduce((a, b) => a + b.outgoingRequests, 0), + totalTime: logs.reduce((a, b) => a + b.totalTime, 0), + incomingBytes: logs.reduce((a, b) => a + b.incomingBytes, 0), + outgoingBytes: logs.reduce((a, b) => a + b.outgoingBytes, 0), + }; + } + @autobind protected async fetchActual(): Promise> { return {}; diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts index 815061c445..965087bc08 100644 --- a/src/services/chart/charts/classes/notes.ts +++ b/src/services/chart/charts/classes/notes.ts @@ -25,6 +25,32 @@ export default class NotesChart extends Chart { }; } + @autobind + protected aggregate(logs: NotesLog[]): NotesLog { + return { + local: { + total: logs[0].local.total, + inc: logs.reduce((a, b) => a + b.local.inc, 0), + dec: logs.reduce((a, b) => a + b.local.dec, 0), + diffs: { + reply: logs.reduce((a, b) => a + b.local.diffs.reply, 0), + renote: logs.reduce((a, b) => a + b.local.diffs.renote, 0), + normal: logs.reduce((a, b) => a + b.local.diffs.normal, 0), + }, + }, + remote: { + total: logs[0].remote.total, + inc: logs.reduce((a, b) => a + b.remote.inc, 0), + dec: logs.reduce((a, b) => a + b.remote.dec, 0), + diffs: { + reply: logs.reduce((a, b) => a + b.remote.diffs.reply, 0), + renote: logs.reduce((a, b) => a + b.remote.diffs.renote, 0), + normal: logs.reduce((a, b) => a + b.remote.diffs.normal, 0), + }, + }, + }; + } + @autobind protected async fetchActual(): Promise> { const [localCount, remoteCount] = await Promise.all([ diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts index aed9f6fce7..e778f7bf61 100644 --- a/src/services/chart/charts/classes/per-user-drive.ts +++ b/src/services/chart/charts/classes/per-user-drive.ts @@ -20,6 +20,18 @@ export default class PerUserDriveChart extends Chart { }; } + @autobind + protected aggregate(logs: PerUserDriveLog[]): PerUserDriveLog { + return { + totalCount: logs[0].totalCount, + totalSize: logs[0].totalSize, + incCount: logs.reduce((a, b) => a + b.incCount, 0), + incSize: logs.reduce((a, b) => a + b.incSize, 0), + decCount: logs.reduce((a, b) => a + b.decCount, 0), + decSize: logs.reduce((a, b) => a + b.decSize, 0), + }; + } + @autobind protected async fetchActual(group: string): Promise> { const [count, size] = await Promise.all([ diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts index 8295c0cb0d..8b536009c8 100644 --- a/src/services/chart/charts/classes/per-user-following.ts +++ b/src/services/chart/charts/classes/per-user-following.ts @@ -35,6 +35,36 @@ export default class PerUserFollowingChart extends Chart { }; } + @autobind + protected aggregate(logs: PerUserFollowingLog[]): PerUserFollowingLog { + return { + local: { + followings: { + total: logs[0].local.followings.total, + inc: logs.reduce((a, b) => a + b.local.followings.inc, 0), + dec: logs.reduce((a, b) => a + b.local.followings.dec, 0), + }, + followers: { + total: logs[0].local.followers.total, + inc: logs.reduce((a, b) => a + b.local.followers.inc, 0), + dec: logs.reduce((a, b) => a + b.local.followers.dec, 0), + }, + }, + remote: { + followings: { + total: logs[0].remote.followings.total, + inc: logs.reduce((a, b) => a + b.remote.followings.inc, 0), + dec: logs.reduce((a, b) => a + b.remote.followings.dec, 0), + }, + followers: { + total: logs[0].remote.followers.total, + inc: logs.reduce((a, b) => a + b.remote.followers.inc, 0), + dec: logs.reduce((a, b) => a + b.remote.followers.dec, 0), + }, + }, + }; + } + @autobind protected async fetchActual(group: string): Promise> { const [ diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts index cccd495604..8d1fb8d2b0 100644 --- a/src/services/chart/charts/classes/per-user-notes.ts +++ b/src/services/chart/charts/classes/per-user-notes.ts @@ -20,6 +20,20 @@ export default class PerUserNotesChart extends Chart { }; } + @autobind + protected aggregate(logs: PerUserNotesLog[]): PerUserNotesLog { + return { + total: logs[0].total, + inc: logs.reduce((a, b) => a + b.inc, 0), + dec: logs.reduce((a, b) => a + b.dec, 0), + diffs: { + reply: logs.reduce((a, b) => a + b.diffs.reply, 0), + renote: logs.reduce((a, b) => a + b.diffs.renote, 0), + normal: logs.reduce((a, b) => a + b.diffs.normal, 0), + }, + }; + } + @autobind protected async fetchActual(group: string): Promise> { const [count] = await Promise.all([ diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts index 124fb4153c..b4cdced40c 100644 --- a/src/services/chart/charts/classes/per-user-reactions.ts +++ b/src/services/chart/charts/classes/per-user-reactions.ts @@ -18,6 +18,18 @@ export default class PerUserReactionsChart extends Chart { return {}; } + @autobind + protected aggregate(logs: PerUserReactionsLog[]): PerUserReactionsLog { + return { + local: { + count: logs.reduce((a, b) => a + b.local.count, 0), + }, + remote: { + count: logs.reduce((a, b) => a + b.remote.count, 0), + }, + }; + } + @autobind protected async fetchActual(group: string): Promise> { return {}; diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts index e32cbcf416..92c8df636e 100644 --- a/src/services/chart/charts/classes/test-grouped.ts +++ b/src/services/chart/charts/classes/test-grouped.ts @@ -21,6 +21,17 @@ export default class TestGroupedChart extends Chart { }; } + @autobind + protected aggregate(logs: TestGroupedLog[]): TestGroupedLog { + return { + foo: { + total: logs[0].foo.total, + inc: logs.reduce((a, b) => a + b.foo.inc, 0), + dec: logs.reduce((a, b) => a + b.foo.dec, 0), + }, + }; + } + @autobind protected async fetchActual(group: string): Promise> { return { diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts index 1eb396c293..5680d713ec 100644 --- a/src/services/chart/charts/classes/test-unique.ts +++ b/src/services/chart/charts/classes/test-unique.ts @@ -15,6 +15,13 @@ export default class TestUniqueChart extends Chart { return {}; } + @autobind + protected aggregate(logs: TestUniqueLog[]): TestUniqueLog { + return { + foo: logs.reduce((a, b) => a.concat(b.foo), [] as TestUniqueLog['foo']), + }; + } + @autobind protected async fetchActual(): Promise> { return {}; @@ -22,8 +29,8 @@ export default class TestUniqueChart extends Chart { @autobind public async uniqueIncrement(key: string) { - await this.incIfUnique({ - foo: 1 - }, 'foos', key); + await this.inc({ + foo: [key] + }); } } diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts index ea64040f3e..d37d298de7 100644 --- a/src/services/chart/charts/classes/test.ts +++ b/src/services/chart/charts/classes/test.ts @@ -21,6 +21,17 @@ export default class TestChart extends Chart { }; } + @autobind + protected aggregate(logs: TestLog[]): TestLog { + return { + foo: { + total: logs[0].foo.total, + inc: logs.reduce((a, b) => a + b.foo.inc, 0), + dec: logs.reduce((a, b) => a + b.foo.dec, 0), + }, + }; + } + @autobind protected async fetchActual(): Promise> { return { diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts index 47e4caa1b7..87b19d88f9 100644 --- a/src/services/chart/charts/classes/users.ts +++ b/src/services/chart/charts/classes/users.ts @@ -25,6 +25,22 @@ export default class UsersChart extends Chart { }; } + @autobind + protected aggregate(logs: UsersLog[]): UsersLog { + return { + local: { + total: logs[0].local.total, + inc: logs.reduce((a, b) => a + b.local.inc, 0), + dec: logs.reduce((a, b) => a + b.local.dec, 0), + }, + remote: { + total: logs[0].remote.total, + inc: logs.reduce((a, b) => a + b.remote.inc, 0), + dec: logs.reduce((a, b) => a + b.remote.dec, 0), + }, + }; + } + @autobind protected async fetchActual(): Promise> { const [localCount, remoteCount] = await Promise.all([ diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts index 6e26bb4698..cdf0579efb 100644 --- a/src/services/chart/charts/schemas/active-users.ts +++ b/src/services/chart/charts/schemas/active-users.ts @@ -1,11 +1,15 @@ export const logSchema = { /** - * アクティブユーザー数 + * アクティブユーザー */ - count: { - type: 'number' as const, + users: { + type: 'array' as const, optional: false as const, nullable: false as const, - description: 'アクティブユーザー数', + description: 'アクティブユーザー', + items: { + type: 'string' as const, + optional: false as const, nullable: false as const, + } }, }; diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts index 4dfd61c97f..791d0d1721 100644 --- a/src/services/chart/charts/schemas/hashtag.ts +++ b/src/services/chart/charts/schemas/hashtag.ts @@ -1,11 +1,15 @@ export const logSchema = { /** - * 投稿された数 + * 投稿したユーザー */ - count: { - type: 'number' as const, + users: { + type: 'array' as const, optional: false as const, nullable: false as const, - description: '投稿された数', + description: '投稿したユーザー', + items: { + type: 'string' as const, + optional: false as const, nullable: false as const, + } }, }; diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts index 075a8092d9..51280400ac 100644 --- a/src/services/chart/charts/schemas/test-unique.ts +++ b/src/services/chart/charts/schemas/test-unique.ts @@ -3,9 +3,12 @@ export const schema = { optional: false as const, nullable: false as const, properties: { foo: { - type: 'number' as const, + type: 'array' as const, optional: false as const, nullable: false as const, - description: '' + items: { + type: 'string' as const, + optional: false as const, nullable: false as const, + } }, } }; diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts index dc09923ae4..10621be073 100644 --- a/src/services/chart/core.ts +++ b/src/services/chart/core.ts @@ -24,8 +24,6 @@ type ArrayValue = { [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue; }; -type Span = 'day' | 'hour'; - type Log = { id: number; @@ -38,22 +36,14 @@ type Log = { * 集計日時のUnixタイムスタンプ(秒) */ date: number; - - /** - * 集計期間 - */ - span: Span; - - /** - * ユニークインクリメント用 - */ - unique?: Record; }; const camelToSnake = (str: string) => { return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); }; +const removeDuplicates = (array: any[]) => Array.from(new Set(array)); + /** * 様々なチャートの管理を司るクラス */ @@ -62,10 +52,21 @@ export default abstract class Chart> { private static readonly columnDot = '_'; private name: string; + private queue: { + diff: DeepPartial; + group: string | null; + }[] = []; public schema: Schema; protected repository: Repository; + protected abstract genNewLog(latest: T): DeepPartial; - protected abstract async fetchActual(group: string | null): Promise>; + + /** + * @param logs 日時が新しい方が先頭 + */ + protected abstract aggregate(logs: T[]): T; + + protected abstract fetchActual(group: string | null): Promise>; @autobind private static convertSchemaToFlatColumnDefinitions(schema: Schema) { @@ -75,10 +76,15 @@ export default abstract class Chart> { const p = path ? `${path}${this.columnDot}${k}` : k; if (v.type === 'object') { flatColumns(v.properties, p); - } else { + } else if (v.type === 'number') { columns[this.columnPrefix + p] = { type: 'bigint', }; + } else if (v.type === 'array' && v.items.type === 'string') { + columns[this.columnPrefix + p] = { + type: 'varchar', + array: true, + }; } } }; @@ -99,11 +105,11 @@ export default abstract class Chart> { @autobind private static convertObjectToFlattenColumns(x: Record) { - const columns = {} as Record; + const columns = {} as Record; const flatten = (x: Obj, path?: string) => { for (const [k, v] of Object.entries(x)) { const p = path ? `${path}${this.columnDot}${k}` : k; - if (typeof v === 'object') { + if (typeof v === 'object' && !Array.isArray(v)) { flatten(v, p); } else { columns[this.columnPrefix + p] = v; @@ -115,14 +121,37 @@ export default abstract class Chart> { } @autobind - private static convertQuery(x: Record) { - const query: Record = {}; + private static countUniqueFields(x: Record) { + const exec = (x: Obj) => { + const res = {} as Record; + for (const [k, v] of Object.entries(x)) { + if (typeof v === 'object' && !Array.isArray(v)) { + res[k] = exec(v); + } else if (Array.isArray(v)) { + res[k] = Array.from(new Set(v)).length; + } else { + res[k] = v; + } + } + return res; + }; + return exec(x); + } - const columns = Chart.convertObjectToFlattenColumns(x); + @autobind + private static convertQuery(diff: Record) { + const query: Record = {}; - for (const [k, v] of Object.entries(columns)) { - if (v > 0) query[k] = () => `"${k}" + ${v}`; - if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`; + for (const [k, v] of Object.entries(diff)) { + if (typeof v === 'number') { + if (v > 0) query[k] = () => `"${k}" + ${v}`; + if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`; + } else if (Array.isArray(v)) { + // TODO: item が文字列以外の場合も対応 + // TODO: item をSQLエスケープ + const items = v.map(item => `"${item}"`).join(','); + query[k] = () => `array_cat("${k}", '{${items}}'::varchar[])`; + } } return query; @@ -169,28 +198,14 @@ export default abstract class Chart> { length: 128, nullable: true }, - span: { - type: 'enum', - enum: ['hour', 'day'] - }, - unique: { - type: 'jsonb', - default: {} - }, ...Chart.convertSchemaToFlatColumnDefinitions(schema) }, indices: [{ columns: ['date'] - }, { - columns: ['span'] }, { columns: ['group'] - }, { - columns: ['span', 'date'] }, { columns: ['date', 'group'] - }, { - columns: ['span', 'date', 'group'] }] }); } @@ -200,7 +215,7 @@ export default abstract class Chart> { this.schema = schema; const entity = Chart.schemaToEntity(name, schema); - const keys = ['span', 'date']; + const keys = ['date']; if (grouped) keys.push('group'); entity.options.uniques = [{ @@ -220,7 +235,8 @@ export default abstract class Chart> { flatColumns(v.properties, p); } else { if (nestedProperty.get(log, p) == null) { - nestedProperty.set(log, p, 0); + const emptyValue = v.type === 'number' ? 0 : []; + nestedProperty.set(log, p, emptyValue); } } } @@ -230,10 +246,9 @@ export default abstract class Chart> { } @autobind - private getLatestLog(span: Span, group: string | null = null): Promise { + private getLatestLog(group: string | null = null): Promise { return this.repository.findOne({ group: group, - span: span }, { order: { date: -1 @@ -242,17 +257,13 @@ export default abstract class Chart> { } @autobind - private async getCurrentLog(span: Span, group: string | null = null): Promise { + private async getCurrentLog(group: string | null = null): Promise { const [y, m, d, h] = Chart.getCurrentDate(); - const current = - span == 'day' ? dateUTC([y, m, d, 0]) : - span == 'hour' ? dateUTC([y, m, d, h]) : - null as never; + const current = dateUTC([y, m, d, h]); - // 現在(今日または今のHour)のログ + // 現在(=今のHour)のログ const currentLog = await this.repository.findOne({ - span: span, date: Chart.dateToTimestamp(current), ...(group ? { group: group } : {}) }); @@ -271,7 +282,7 @@ export default abstract class Chart> { // * 昨日何もチャートを更新するような出来事がなかった場合は、 // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 // * 「昨日の」と決め打ちせずに「もっとも最近の」とします - const latest = await this.getLatestLog(span, group); + const latest = await this.getLatestLog(group); if (latest != null) { const obj = Chart.convertFlattenColumnsToObject( @@ -286,17 +297,16 @@ export default abstract class Chart> { // 初期ログデータを作成 data = this.getNewLog(null); - logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`); + logger.info(`${this.name + (group ? `:${group}` : '')}: Initial commit created`); } const date = Chart.dateToTimestamp(current); - const lockKey = `${this.name}:${date}:${group}:${span}`; + const lockKey = `${this.name}:${date}:${group}`; const unlock = await getChartInsertLock(lockKey); try { // ロック内でもう1回チェックする const currentLog = await this.repository.findOne({ - span: span, date: date, ...(group ? { group: group } : {}) }); @@ -307,12 +317,11 @@ export default abstract class Chart> { // 新規ログ挿入 log = await this.repository.save({ group: group, - span: span, date: date, ...Chart.convertObjectToFlattenColumns(data) }); - logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`); + logger.info(`${this.name + (group ? `:${group}` : '')}: New commit created`); return log; } finally { @@ -321,38 +330,62 @@ export default abstract class Chart> { } @autobind - protected commit(query: Record, group: string | null = null, uniqueKey?: string, uniqueValue?: string): Promise { + protected commit(diff: DeepPartial, group: string | null = null): void { + this.queue.push({ + diff, group, + }); + } + + @autobind + public async save() { + if (this.queue.length === 0) { + logger.info(`${this.name}: Write skipped`); + return; + } + + // TODO: 前の時間のログがqueueにあった場合のハンドリング + // 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。 + // 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが queue に追加されたとすると、 + // そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。 + // これを回避するための実装は複雑になりそうなため、一旦保留。 + const update = async (log: Log) => { - // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く - if ( - uniqueKey && log.unique && - log.unique[uniqueKey] && - log.unique[uniqueKey].includes(uniqueValue) - ) return; - - // ユニークインクリメントの指定のキーに値を追加 - if (uniqueKey && log.unique) { - if (log.unique[uniqueKey]) { - const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`; - query['unique'] = () => sql; - } else { - const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`; - query['unique'] = () => sql; + const finalDiffs = {} as Record; + + for (const diff of this.queue.filter(q => q.group === log.group).map(q => q.diff)) { + const columns = Chart.convertObjectToFlattenColumns(diff); + + for (const [k, v] of Object.entries(columns)) { + if (finalDiffs[k] == null) { + finalDiffs[k] = v; + } else { + if (typeof finalDiffs[k] === 'number') { + (finalDiffs[k] as number) += v as number; + } else { + (finalDiffs[k] as unknown[]) = (finalDiffs[k] as unknown[]).concat(v); + } + } } } + const query = Chart.convertQuery(finalDiffs); + // ログ更新 await this.repository.createQueryBuilder() .update() .set(query) .where('id = :id', { id: log.id }) .execute(); + + logger.info(`${this.name + (log.group ? `:${log.group}` : '')}: Updated`); + + // TODO: この一連の処理が始まった後に新たにqueueに入ったものは消さないようにする + this.queue = this.queue.filter(q => q.group !== log.group); }; - return Promise.all([ - this.getCurrentLog('day', group).then(log => update(log)), - this.getCurrentLog('hour', group).then(log => update(log)), - ]); + const groups = removeDuplicates(this.queue.map(log => log.group)); + + await Promise.all(groups.map(group => this.getCurrentLog(group).then(log => update(log)))); } @autobind @@ -367,39 +400,30 @@ export default abstract class Chart> { .execute(); }; - return Promise.all([ - this.getCurrentLog('day', group).then(log => update(log)), - this.getCurrentLog('hour', group).then(log => update(log)), - ]); + return this.getCurrentLog(group).then(log => update(log)); } @autobind protected async inc(inc: DeepPartial, group: string | null = null): Promise { - await this.commit(Chart.convertQuery(inc as any), group); + await this.commit(inc, group); } @autobind - protected async incIfUnique(inc: DeepPartial, key: string, value: string, group: string | null = null): Promise { - await this.commit(Chart.convertQuery(inc as any), group, key, value); - } - - @autobind - public async getChart(span: Span, amount: number, begin: Date | null, group: string | null = null): Promise> { - const [y, m, d, h, _m, _s, _ms] = begin ? Chart.parseDate(subtractTime(addTime(begin, 1, span), 1)) : Chart.getCurrentDate(); - const [y2, m2, d2, h2] = begin ? Chart.parseDate(addTime(begin, 1, span)) : [] as never; + public async getChart(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise> { + const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate(); + const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never; const lt = dateUTC([y, m, d, h, _m, _s, _ms]); const gt = - span === 'day' ? subtractTime(begin ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : - span === 'hour' ? subtractTime(begin ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : + span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : + span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : null as never; // ログ取得 let logs = await this.repository.find({ where: { group: group, - span: span, date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)) }, order: { @@ -413,7 +437,6 @@ export default abstract class Chart> { // (すくなくともひとつログが無いと隙間埋めできないため) const recentLog = await this.repository.findOne({ group: group, - span: span }, { order: { date: -1 @@ -430,7 +453,6 @@ export default abstract class Chart> { // (隙間埋めできないため) const outdatedLog = await this.repository.findOne({ group: group, - span: span, date: LessThan(Chart.dateToTimestamp(gt)) }, { order: { @@ -445,23 +467,56 @@ export default abstract class Chart> { const chart: T[] = []; - // 整形 - for (let i = (amount - 1); i >= 0; i--) { - const current = - span === 'day' ? subtractTime(dateUTC([y, m, d, 0]), i, 'day') : - span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') : - null as never; - - const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); - - if (log) { - const data = Chart.convertFlattenColumnsToObject(log as Record); - chart.unshift(data); - } else { - // 隙間埋め - const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); - const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record) : null; - chart.unshift(this.getNewLog(data)); + if (span === 'hour') { + for (let i = (amount - 1); i >= 0; i--) { + const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour'); + + const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); + + if (log) { + const data = Chart.convertFlattenColumnsToObject(log as Record); + chart.unshift(Chart.countUniqueFields(data)); + } else { + // 隙間埋め + const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); + const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record) : null; + chart.unshift(Chart.countUniqueFields(this.getNewLog(data))); + } + } + } else if (span === 'day') { + const logsForEachDays: T[][] = []; + let currentDay = -1; + let currentDayIndex = -1; + for (let i = ((amount - 1) * 24) + h; i >= 0; i--) { + const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour'); + const _currentDay = Chart.parseDate(current)[2]; + if (currentDay != _currentDay) currentDayIndex++; + currentDay = _currentDay; + + const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); + + if (log) { + if (logsForEachDays[currentDayIndex]) { + logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log)); + } else { + logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log)]; + } + } else { + // 隙間埋め + const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); + const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record) : null; + const newLog = this.getNewLog(data); + if (logsForEachDays[currentDayIndex]) { + logsForEachDays[currentDayIndex].unshift(newLog); + } else { + logsForEachDays[currentDayIndex] = [newLog]; + } + } + } + + for (const logs of logsForEachDays) { + const log = this.aggregate(logs); + chart.unshift(Chart.countUniqueFields(log)); } } @@ -473,20 +528,19 @@ export default abstract class Chart> { * { foo: [1, 2, 3], bar: [5, 6, 7] } * にする */ - const dive = (x: Obj, path?: string) => { + const compact = (x: Obj, path?: string) => { for (const [k, v] of Object.entries(x)) { const p = path ? `${path}.${k}` : k; - if (typeof v == 'object') { - dive(v, p); + if (typeof v === 'object' && !Array.isArray(v)) { + compact(v, p); } else { - const values = chart.map(s => nestedProperty.get(s, p)) - .map(v => parseInt(v, 10)); // TypeORMのバグ(?)で何故か数値カラムの値が文字列型になっているので数値に戻す + const values = chart.map(s => nestedProperty.get(s, p)); nestedProperty.set(res, p, values); } } }; - dive(chart[0]); + compact(chart[0]); return res; } diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts index 9626e3d6b3..dde02bd64d 100644 --- a/src/services/chart/index.ts +++ b/src/services/chart/index.ts @@ -10,6 +10,7 @@ import PerUserReactionsChart from './charts/classes/per-user-reactions'; import HashtagChart from './charts/classes/hashtag'; import PerUserFollowingChart from './charts/classes/per-user-following'; import PerUserDriveChart from './charts/classes/per-user-drive'; +import { beforeShutdown } from '../../misc/before-shutdown'; export const federationChart = new FederationChart(); export const notesChart = new NotesChart(); @@ -23,3 +24,27 @@ export const perUserReactionsChart = new PerUserReactionsChart(); export const hashtagChart = new HashtagChart(); export const perUserFollowingChart = new PerUserFollowingChart(); export const perUserDriveChart = new PerUserDriveChart(); + +const charts = [ + federationChart, + notesChart, + usersChart, + networkChart, + activeUsersChart, + instanceChart, + perUserNotesChart, + driveChart, + perUserReactionsChart, + hashtagChart, + perUserFollowingChart, + perUserDriveChart, +]; + +// 20分おきにメモリ情報をDBに書き込み +setInterval(() => { + for (const chart of charts) { + chart.save(); + } +}, 1000 * 60 * 20); + +beforeShutdown(() => Promise.all(charts.map(chart => chart.save()))); diff --git a/test/chart.ts b/test/chart.ts index 25b083db17..55f6bd696c 100644 --- a/test/chart.ts +++ b/test/chart.ts @@ -72,7 +72,7 @@ describe('Chart', () => { testUniqueChart = new TestUniqueChart(); clock = lolex.install({ - now: new Date('2000-01-01 00:00:00') + now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)) }); done(); }); @@ -85,6 +85,7 @@ describe('Chart', () => { it('Can updates', async(async () => { await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -105,9 +106,10 @@ describe('Chart', () => { }, }); })); - + it('Can updates (dec)', async(async () => { await testChart.decrement(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -154,6 +156,7 @@ describe('Chart', () => { await testChart.increment(); await testChart.increment(); await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -177,10 +180,12 @@ describe('Chart', () => { it('Can updates at different times', async(async () => { await testChart.increment(); + await testChart.save(); clock.tick('01:00:00'); await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -202,12 +207,45 @@ describe('Chart', () => { }); })); + // 仕様上はこうなってほしいけど、実装は難しそうなのでskip + /* + it('Can updates at different times without save', async(async () => { + await testChart.increment(); + + clock.tick('01:00:00'); + + await testChart.increment(); + await testChart.save(); + + const chartHours = await testChart.getChart('hour', 3, null); + const chartDays = await testChart.getChart('day', 3, null); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 1, 0], + total: [2, 1, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + */ + it('Can padding', async(async () => { await testChart.increment(); + await testChart.save(); clock.tick('02:00:00'); await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -232,6 +270,7 @@ describe('Chart', () => { // 要求された範囲にログがひとつもない場合でもパディングできる it('Can padding from past range', async(async () => { await testChart.increment(); + await testChart.save(); clock.tick('05:00:00'); @@ -259,8 +298,12 @@ describe('Chart', () => { // Issue #3190 it('Can padding from past range 2', async(async () => { await testChart.increment(); + await testChart.save(); + clock.tick('05:00:00'); + await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -284,10 +327,12 @@ describe('Chart', () => { it('Can specify offset', async(async () => { await testChart.increment(); + await testChart.save(); clock.tick('01:00:00'); await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); @@ -313,10 +358,12 @@ describe('Chart', () => { clock.tick('00:30:00'); await testChart.increment(); + await testChart.save(); clock.tick('01:30:00'); await testChart.increment(); + await testChart.save(); const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0))); @@ -341,6 +388,7 @@ describe('Chart', () => { describe('Grouped', () => { it('Can updates', async(async () => { await testGroupedChart.increment('alice'); + await testGroupedChart.save(); const aliceChartHours = await testGroupedChart.getChart('hour', 3, null, 'alice'); const aliceChartDays = await testGroupedChart.getChart('day', 3, null, 'alice'); @@ -386,6 +434,7 @@ describe('Chart', () => { await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('bob'); + await testUniqueChart.save(); const chartHours = await testUniqueChart.getChart('hour', 3, null); const chartDays = await testUniqueChart.getChart('day', 3, null); @@ -428,6 +477,7 @@ describe('Chart', () => { it('Can resync (2)', async(async () => { await testChart.increment(); + await testChart.save(); clock.tick('01:00:00'); diff --git a/yarn.lock b/yarn.lock index 274f07631c..c838362820 100644 --- a/yarn.lock +++ b/yarn.lock @@ -358,10 +358,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== +"@sinonjs/fake-timers@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.0.2.tgz#a53e71d4154ee704ea9b36a6d0b0780e246fadd1" + integrity sha512-dF84L5YC90gIOegPDCYymPIsDmwMWWSh7BwfDXQYePi8lVIEp7IZ1UVGkME8FjXOsDPxan12x4aaK+Lo6wVh9A== dependencies: "@sinonjs/commons" "^1.7.0" -- cgit v1.2.3-freya From 9cc8b3656aa016a6443980488b3cb5d1e431fb34 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Mar 2021 13:33:14 +0900 Subject: lint --- src/misc/before-shutdown.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/misc') diff --git a/src/misc/before-shutdown.ts b/src/misc/before-shutdown.ts index 58d0ea5108..8639d42b04 100644 --- a/src/misc/before-shutdown.ts +++ b/src/misc/before-shutdown.ts @@ -32,7 +32,9 @@ const shutdownListeners = []; * @param {function(string)} fn Function to execute on shutdown. */ const processOnce = (signals, fn) => { - return signals.forEach(sig => process.once(sig, fn)); + for (const sig of signals) { + process.once(sig, fn); + } }; /** -- cgit v1.2.3-freya From 5e61c60f85e262421c1655eb6f8a317b88ccb88f Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 19 Mar 2021 18:22:34 +0900 Subject: perf(server): Improver performance --- src/misc/fetch-meta.ts | 2 +- src/server/api/endpoints/i/notifications.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src/misc') diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts index 680cf37a72..e7a945dc9e 100644 --- a/src/misc/fetch-meta.ts +++ b/src/misc/fetch-meta.ts @@ -32,4 +32,4 @@ setInterval(() => { fetchMeta(true).then(meta => { cache = meta; }); -}, 5000); +}, 1000 * 10); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 0e09bc73fd..7a423edb8d 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -85,7 +85,9 @@ export default define(meta, async (ps, user) => { const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) .andWhere(`notification.notifieeId = :meId`, { meId: user.id }) - .leftJoinAndSelect('notification.notifier', 'notifier'); + .leftJoinAndSelect('notification.notifier', 'notifier') + .leftJoinAndSelect('notification.note', 'note') + .leftJoinAndSelect('note.user', 'user'); query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`); query.setParameters(mutingQuery.getParameters()); -- cgit v1.2.3-freya From d1efe1d2085dbae14f85ab6a993e755926067446 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Mon, 22 Mar 2021 00:44:38 +0900 Subject: populateEmojisのリファクタと絵文字情報のキャッシュ (#7378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * revert * Refactor populateEmojis, Cache emojis * ん * fix typo * コメント --- src/misc/cache.ts | 23 +++++- src/misc/populate-emojis.ts | 58 +++++++++++++ src/models/repositories/note.ts | 140 ++------------------------------ src/models/repositories/notification.ts | 45 +--------- src/models/repositories/user.ts | 39 +-------- 5 files changed, 89 insertions(+), 216 deletions(-) create mode 100644 src/misc/populate-emojis.ts (limited to 'src/misc') diff --git a/src/misc/cache.ts b/src/misc/cache.ts index 5b7017a3b9..71fbbd8a4c 100644 --- a/src/misc/cache.ts +++ b/src/misc/cache.ts @@ -14,13 +14,30 @@ export class Cache { }); } - public get(key: string | null): T | null { + public get(key: string | null): T | undefined { const cached = this.cache.get(key); - if (cached == null) return null; + if (cached == null) return undefined; if ((Date.now() - cached.date) > this.lifetime) { this.cache.delete(key); - return null; + return undefined; } return cached.value; } + + public delete(key: string | null) { + this.cache.delete(key); + } + + public async fetch(key: string | null, fetcher: () => Promise): Promise { + const cachedValue = this.get(key); + if (cachedValue !== undefined) { + // Cache HIT + return cachedValue; + } + + // Cache MISS + const value = await fetcher(); + this.set(key, value); + return value; + } } diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts new file mode 100644 index 0000000000..6300cfb95e --- /dev/null +++ b/src/misc/populate-emojis.ts @@ -0,0 +1,58 @@ +import { Emojis } from '../models'; +import { Emoji } from '../models/entities/emoji'; +import { Cache } from './cache'; +import { isSelfHost, toPunyNullable } from './convert-host'; + +const cache = new Cache(1000 * 60 * 60); + +/** + * 添付用絵文字情報 + */ +type PopulatedEmoji = { + name: string; + url: string; +}; + +/** + * 添付用絵文字情報を解決する + * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能)) + * @param noteUserHost ノートやユーザープロフィールの所有者 + * @returns 絵文字情報, nullは未マッチを意味する + */ +export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise { + const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); + if (!match) return null; + + const name = match[1]; + + // クエリに使うホスト + let host = match[2] === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) + : match[2] === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) + : isSelfHost(match[2]) ? null // 自ホスト指定 + : (match[2] || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない) + + host = toPunyNullable(host); + + const queryOrNull = async () => (await Emojis.findOne({ + name, + host + })) || null; + + const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); + + if (emoji == null) return null; + + return { + name: emojiName, + url: emoji.url, + }; +} + +/** + * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される) + */ +export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise { + const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost))); + return emojis.filter((x): x is PopulatedEmoji => x != null); +} + diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 1fcedbd56f..9771f66704 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -1,15 +1,14 @@ import { EntityRepository, Repository, In } from 'typeorm'; import { Note } from '../entities/note'; import { User } from '../entities/user'; -import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..'; +import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..'; import { SchemaType } from '../../misc/schema'; import { awaitAll } from '../../prelude/await-all'; import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '../../misc/reaction-lib'; import { toString } from '../../mfm/to-string'; import { parse } from '../../mfm/parse'; -import { Emoji } from '../entities/emoji'; -import { concat } from '../../prelude/array'; import { NoteReaction } from '../entities/note-reaction'; +import { populateEmojis } from '../../misc/populate-emojis'; export type PackedNote = SchemaType; @@ -85,7 +84,6 @@ export class NoteRepository extends Repository { detail?: boolean; skipHide?: boolean; _hint_?: { - emojis: Emoji[] | null; myReactions: Map; }; } @@ -135,93 +133,6 @@ export class NoteRepository extends Repository { }; } - /** - * 添付用emojisを解決する - * @param emojiNames Note等に添付されたカスタム絵文字名 (:は含めない) - * @param noteUserHost Noteのホスト - * @param reactionNames Note等にリアクションされたカスタム絵文字名 (:は含めない) - */ - async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) { - const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name); - - let all = [] as { - name: string, - url: string - }[]; - - // 与えられたhintだけで十分(=新たにクエリする必要がない)かどうかを表すフラグ - let enough = true; - if (options?._hint_?.emojis) { - for (const name of emojiNames) { - const matched = options._hint_.emojis.find(x => x.name === name && x.host === noteUserHost); - if (matched) { - all.push({ - name: matched.name, - url: matched.url, - }); - } else { - enough = false; - } - } - for (const customReaction of customReactions) { - const matched = options._hint_.emojis.find(x => x.name === customReaction.name && x.host === customReaction.host); - if (matched) { - all.push({ - name: `${matched.name}@${matched.host || '.'}`, // @host付きでローカルは. - url: matched.url, - }); - } else { - enough = false; - } - } - } else { - enough = false; - } - if (enough) return all; - - // カスタム絵文字 - if (emojiNames?.length > 0) { - const tmp = await Emojis.find({ - where: { - name: In(emojiNames), - host: noteUserHost - }, - select: ['name', 'host', 'url'] - }).then(emojis => emojis.map((emoji: Emoji) => { - return { - name: emoji.name, - url: emoji.url, - }; - })); - - all = concat([all, tmp]); - } - - if (customReactions?.length > 0) { - const where = [] as {}[]; - - for (const customReaction of customReactions) { - where.push({ - name: customReaction.name, - host: customReaction.host - }); - } - - const tmp = await Emojis.find({ - where, - select: ['name', 'host', 'url'] - }).then(emojis => emojis.map((emoji: Emoji) => { - return { - name: `${emoji.name}@${emoji.host || '.'}`, // @host付きでローカルは. - url: emoji.url, - }; - })); - all = concat([all, tmp]); - } - - return all; - } - async function populateMyReaction() { if (options?._hint_?.myReactions) { const reaction = options._hint_.myReactions.get(note.id); @@ -257,15 +168,14 @@ export class NoteRepository extends Repository { : await Channels.findOne(note.channelId) : null; + const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, '')); + const packed = await awaitAll({ id: note.id, createdAt: note.createdAt.toISOString(), userId: note.userId, user: Users.pack(note.user || note.userId, meId, { detail: false, - _hint_: { - emojis: options?._hint_?.emojis || null - } }), text: text, cw: note.cw, @@ -277,7 +187,7 @@ export class NoteRepository extends Repository { repliesCount: note.repliesCount, reactions: convertLegacyReactions(note.reactions), tags: note.tags.length > 0 ? note.tags : undefined, - emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)), + emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host), fileIds: note.fileIds, files: DriveFiles.packMany(note.fileIds), replyId: note.replyId, @@ -350,48 +260,10 @@ export class NoteRepository extends Repository { } } - // TODO: ここら辺の処理をaggregateEmojisみたいな関数に切り出したい - let emojisWhere: any[] = []; - for (const note of notes) { - if (typeof note !== 'object') continue; - emojisWhere.push({ - name: In(note.emojis), - host: note.userHost - }); - if (note.renote) { - emojisWhere.push({ - name: In(note.renote.emojis), - host: note.renote.userHost - }); - if (note.renote.user) { - emojisWhere.push({ - name: In(note.renote.user.emojis), - host: note.renote.userHost - }); - } - } - const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name); - emojisWhere = emojisWhere.concat(customReactions.map(x => ({ - name: x.name, - host: x.host - }))); - if (note.user) { - emojisWhere.push({ - name: In(note.user.emojis), - host: note.userHost - }); - } - } - const emojis = emojisWhere.length > 0 ? await Emojis.find({ - where: emojisWhere, - select: ['name', 'host', 'url'] - }) : null; - return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { - myReactions: myReactionsMap, - emojis: emojis + myReactions: myReactionsMap } }))); } diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts index 986ddb1d42..4f6e797ef9 100644 --- a/src/models/repositories/notification.ts +++ b/src/models/repositories/notification.ts @@ -1,13 +1,11 @@ import { EntityRepository, In, Repository } from 'typeorm'; -import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions, Emojis } from '..'; +import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '..'; import { Notification } from '../entities/notification'; import { awaitAll } from '../../prelude/await-all'; import { SchemaType } from '../../misc/schema'; import { Note } from '../entities/note'; import { NoteReaction } from '../entities/note-reaction'; import { User } from '../entities/user'; -import { decodeReaction } from '../../misc/reaction-lib'; -import { Emoji } from '../entities/emoji'; export type PackedNotification = SchemaType; @@ -17,7 +15,6 @@ export class NotificationRepository extends Repository { src: Notification['id'] | Notification, options: { _hintForEachNotes_?: { - emojis: Emoji[] | null; myReactions: Map; }; } @@ -101,47 +98,9 @@ export class NotificationRepository extends Repository { myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null); } - // TODO: ここら辺の処理をaggregateEmojisみたいな関数に切り出したい - let emojisWhere: any[] = []; - for (const note of notes) { - if (typeof note !== 'object') continue; - emojisWhere.push({ - name: In(note.emojis), - host: note.userHost - }); - if (note.renote) { - emojisWhere.push({ - name: In(note.renote.emojis), - host: note.renote.userHost - }); - if (note.renote.user) { - emojisWhere.push({ - name: In(note.renote.user.emojis), - host: note.renote.userHost - }); - } - } - const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name); - emojisWhere = emojisWhere.concat(customReactions.map(x => ({ - name: x.name, - host: x.host - }))); - if (note.user) { - emojisWhere.push({ - name: In(note.user.emojis), - host: note.userHost - }); - } - } - const emojis = emojisWhere.length > 0 ? await Emojis.find({ - where: emojisWhere, - select: ['name', 'host', 'url'] - }) : null; - return await Promise.all(notifications.map(x => this.pack(x, { _hintForEachNotes_: { - myReactions: myReactionsMap, - emojis: emojis, + myReactions: myReactionsMap } }))); } diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 19b0e54239..ffece291d6 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -1,11 +1,11 @@ import $ from 'cafy'; import { EntityRepository, Repository, In, Not } from 'typeorm'; import { User, ILocalUser, IRemoteUser } from '../entities/user'; -import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..'; +import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..'; import config from '../../config'; import { SchemaType } from '../../misc/schema'; import { awaitAll } from '../../prelude/await-all'; -import { Emoji } from '../entities/emoji'; +import { populateEmojis } from '../../misc/populate-emojis'; export type PackedUser = SchemaType; @@ -150,9 +150,6 @@ export class UserRepository extends Repository { options?: { detail?: boolean, includeSecrets?: boolean, - _hint_?: { - emojis: Emoji[] | null; - }; } ): Promise { const opts = Object.assign({ @@ -170,34 +167,6 @@ export class UserRepository extends Repository { }) : []; const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null; - let emojis: Emoji[] = []; - if (user.emojis.length > 0) { - // 与えられたhintだけで十分(=新たにクエリする必要がない)かどうかを表すフラグ - let enough = true; - if (options?._hint_?.emojis) { - for (const name of user.emojis) { - const matched = options._hint_.emojis.find(x => x.name === name && x.host === user.host); - if (matched) { - emojis.push(matched); - } else { - enough = false; - } - } - } else { - enough = false; - } - - if (!enough) { - emojis = await Emojis.find({ - where: { - name: In(user.emojis), - host: user.host - }, - select: ['name', 'host', 'url', 'aliases'] - }); - } - } - const falsy = opts.detail ? false : undefined; const packed = { @@ -220,9 +189,7 @@ export class UserRepository extends Repository { faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, } : undefined) : undefined, - - // カスタム絵文字添付 - emojis: emojis, + emojis: populateEmojis(user.emojis, user.host), ...(opts.detail ? { url: profile!.url, -- cgit v1.2.3-freya From 9e6cdd6106358f703dc8ce89cd1ae223a261f0cb Mon Sep 17 00:00:00 2001 From: mei23 Date: Mon, 22 Mar 2021 00:45:14 +0900 Subject: のホスト MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/misc/populate-emojis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/misc') diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts index 6300cfb95e..f3056fdebd 100644 --- a/src/misc/populate-emojis.ts +++ b/src/misc/populate-emojis.ts @@ -16,7 +16,7 @@ type PopulatedEmoji = { /** * 添付用絵文字情報を解決する * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能)) - * @param noteUserHost ノートやユーザープロフィールの所有者 + * @param noteUserHost ノートやユーザープロフィールの所有者のホスト * @returns 絵文字情報, nullは未マッチを意味する */ export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise { -- cgit v1.2.3-freya From 1f0abef0843a3da364965a3327e7c0eba00b015a Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 22 Mar 2021 12:36:57 +0900 Subject: refactor: extract functions --- src/misc/populate-emojis.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) (limited to 'src/misc') diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts index f3056fdebd..f21e24b219 100644 --- a/src/misc/populate-emojis.ts +++ b/src/misc/populate-emojis.ts @@ -13,6 +13,30 @@ type PopulatedEmoji = { url: string; }; +function normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { + // クエリに使うホスト + let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) + : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) + : isSelfHost(src) ? null // 自ホスト指定 + : (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない) + + host = toPunyNullable(host); + + return host; +} + +function parseEmojiStr(emojiName: string, noteUserHost: string | null) { + const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); + if (!match) return { name: null, host: null }; + + const name = match[1]; + + // ホスト正規化 + const host = toPunyNullable(normalizeHost(match[2], noteUserHost)); + + return { name, host }; +} + /** * 添付用絵文字情報を解決する * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能)) @@ -20,18 +44,8 @@ type PopulatedEmoji = { * @returns 絵文字情報, nullは未マッチを意味する */ export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise { - const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); - if (!match) return null; - - const name = match[1]; - - // クエリに使うホスト - let host = match[2] === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) - : match[2] === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) - : isSelfHost(match[2]) ? null // 自ホスト指定 - : (match[2] || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない) - - host = toPunyNullable(host); + const { name, host } = parseEmojiStr(emojiName, noteUserHost); + if (name == null) return null; const queryOrNull = async () => (await Emojis.findOne({ name, -- cgit v1.2.3-freya From 8f41dfec2e27c35809f97399cee7e4ad6be21056 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 22 Mar 2021 12:41:33 +0900 Subject: perf(server): Reduce database query --- src/misc/populate-emojis.ts | 47 +++++++++++++++++++++++++++++++++ src/models/repositories/note.ts | 4 ++- src/models/repositories/notification.ts | 3 +++ 3 files changed, 53 insertions(+), 1 deletion(-) (limited to 'src/misc') diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts index f21e24b219..6b74430182 100644 --- a/src/misc/populate-emojis.ts +++ b/src/misc/populate-emojis.ts @@ -1,7 +1,10 @@ +import { In } from 'typeorm'; import { Emojis } from '../models'; import { Emoji } from '../models/entities/emoji'; +import { Note } from '../models/entities/note'; import { Cache } from './cache'; import { isSelfHost, toPunyNullable } from './convert-host'; +import { decodeReaction } from './reaction-lib'; const cache = new Cache(1000 * 60 * 60); @@ -70,3 +73,47 @@ export async function populateEmojis(emojiNames: string[], noteUserHost: string return emojis.filter((x): x is PopulatedEmoji => x != null); } +export function aggregateNoteEmojis(notes: Note[]) { + let emojis: { name: string | null; host: string | null; }[] = []; + for (const note of notes) { + emojis = emojis.concat(note.emojis + .map(e => parseEmojiStr(e, note.userHost))); + if (note.renote) { + emojis = emojis.concat(note.renote.emojis + .map(e => parseEmojiStr(e, note.renote!.userHost))); + if (note.renote.user) { + emojis = emojis.concat(note.renote.user.emojis + .map(e => parseEmojiStr(e, note.renote!.userHost))); + } + } + const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name != null) as typeof emojis; + emojis = emojis.concat(customReactions); + if (note.user) { + emojis = emojis.concat(note.user.emojis + .map(e => parseEmojiStr(e, note.userHost))); + } + } + return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[]; +} + +/** + * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します + */ +export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { + const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null); + const emojisQuery: any[] = []; + const hosts = new Set(notCachedEmojis.map(e => e.host)); + for (const host of hosts) { + emojisQuery.push({ + name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)), + host: host + }); + } + const _emojis = emojisQuery.length > 0 ? await Emojis.find({ + where: emojisQuery, + select: ['name', 'host', 'url'] + }) : []; + for (const emoji of _emojis) { + cache.set(`${emoji.name} ${emoji.host}`, emoji); + } +} diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 354ea4dc70..73e18f6c5b 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -8,7 +8,7 @@ import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '. import { toString } from '../../mfm/to-string'; import { parse } from '../../mfm/parse'; import { NoteReaction } from '../entities/note-reaction'; -import { populateEmojis } from '../../misc/populate-emojis'; +import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '../../misc/populate-emojis'; export type PackedNote = SchemaType; @@ -259,6 +259,8 @@ export class NoteRepository extends Repository { } } + await prefetchEmojis(aggregateNoteEmojis(notes)); + return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts index 4f6e797ef9..83fe11d5f7 100644 --- a/src/models/repositories/notification.ts +++ b/src/models/repositories/notification.ts @@ -6,6 +6,7 @@ import { SchemaType } from '../../misc/schema'; import { Note } from '../entities/note'; import { NoteReaction } from '../entities/note-reaction'; import { User } from '../entities/user'; +import { aggregateNoteEmojis, prefetchEmojis } from '../../misc/populate-emojis'; export type PackedNotification = SchemaType; @@ -98,6 +99,8 @@ export class NotificationRepository extends Repository { myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null); } + await prefetchEmojis(aggregateNoteEmojis(notes)); + return await Promise.all(notifications.map(x => this.pack(x, { _hintForEachNotes_: { myReactions: myReactionsMap -- cgit v1.2.3-freya From 202e943d559d7c3a50a63aaca810dd3141ab70c4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 22 Mar 2021 12:46:46 +0900 Subject: tweak cache lifetime --- src/misc/populate-emojis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/misc') diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts index 6b74430182..8052c71489 100644 --- a/src/misc/populate-emojis.ts +++ b/src/misc/populate-emojis.ts @@ -6,7 +6,7 @@ import { Cache } from './cache'; import { isSelfHost, toPunyNullable } from './convert-host'; import { decodeReaction } from './reaction-lib'; -const cache = new Cache(1000 * 60 * 60); +const cache = new Cache(1000 * 60 * 60 * 12); /** * 添付用絵文字情報 -- cgit v1.2.3-freya From 7c3086e9d9508d5df03d7859932c766a26b9664e Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 22 Mar 2021 15:14:54 +0900 Subject: perf(server): Cache user keypair --- src/misc/keypair-store.ts | 10 ++++++++++ src/remote/activitypub/renderer/index.ts | 6 ++---- src/remote/activitypub/renderer/person.ts | 5 +++-- src/remote/activitypub/request.ts | 10 +++------- src/server/activitypub.ts | 5 +++-- 5 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 src/misc/keypair-store.ts (limited to 'src/misc') diff --git a/src/misc/keypair-store.ts b/src/misc/keypair-store.ts new file mode 100644 index 0000000000..43f451110c --- /dev/null +++ b/src/misc/keypair-store.ts @@ -0,0 +1,10 @@ +import { UserKeypairs } from '../models'; +import { User } from '../models/entities/user'; +import { UserKeypair } from '../models/entities/user-keypair'; +import { Cache } from './cache'; + +const cache = new Cache(Infinity); + +export async function getUserKeypair(userId: User['id']): Promise { + return await cache.fetch(userId, async () => await UserKeypairs.findOneOrFail(userId)); +} diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts index e74affdadf..4c33fdafb1 100644 --- a/src/remote/activitypub/renderer/index.ts +++ b/src/remote/activitypub/renderer/index.ts @@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid'; import { IActivity } from '../type'; import { LdSignature } from '../misc/ld-signature'; import { ILocalUser } from '../../../models/entities/user'; -import { UserKeypairs } from '../../../models'; +import { getUserKeypair } from '../../../misc/keypair-store'; export const renderActivity = (x: any): IActivity | null => { if (x == null) return null; @@ -23,9 +23,7 @@ export const renderActivity = (x: any): IActivity | null => { export const attachLdSignature = async (activity: any, user: ILocalUser): Promise => { if (activity == null) return null; - const keypair = await UserKeypairs.findOneOrFail({ - userId: user.id - }); + const keypair = await getUserKeypair(user.id); const obj = { // as non-standards diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 4907e3bc6f..479e6d76bf 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -8,7 +8,8 @@ import { getEmojis } from './note'; import renderEmoji from './emoji'; import { IIdentifier } from '../models/identifier'; import renderHashtag from './hashtag'; -import { DriveFiles, UserProfiles, UserKeypairs } from '../../../models'; +import { DriveFiles, UserProfiles } from '../../../models'; +import { getUserKeypair } from '../../../misc/keypair-store'; export async function renderPerson(user: ILocalUser) { const id = `${config.url}/users/${user.id}`; @@ -49,7 +50,7 @@ export async function renderPerson(user: ILocalUser) { ...hashtagTags, ]; - const keypair = await UserKeypairs.findOneOrFail(user.id); + const keypair = await getUserKeypair(user.id); const person = { type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 2f07351635..5f15d5480c 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -5,11 +5,11 @@ import * as crypto from 'crypto'; import config from '../../config'; import { ILocalUser } from '../../models/entities/user'; -import { UserKeypairs } from '../../models'; import { getAgentByUrl } from '../../misc/fetch'; import { URL } from 'url'; import got from 'got'; import * as Got from 'got'; +import { getUserKeypair } from '../../misc/keypair-store'; export default async (user: ILocalUser, url: string, object: any) => { const timeout = 10 * 1000; @@ -22,9 +22,7 @@ export default async (user: ILocalUser, url: string, object: any) => { sha256.update(data); const hash = sha256.digest('base64'); - const keypair = await UserKeypairs.findOneOrFail({ - userId: user.id - }); + const keypair = await getUserKeypair(user.id); await new Promise((resolve, reject) => { const req = https.request({ @@ -74,9 +72,7 @@ export default async (user: ILocalUser, url: string, object: any) => { export async function signedGet(url: string, user: ILocalUser) { const timeout = 10 * 1000; - const keypair = await UserKeypairs.findOneOrFail({ - userId: user.id - }); + const keypair = await getUserKeypair(user.id); const req = got.get(url, { headers: { diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index bf71258625..694807239b 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -13,10 +13,11 @@ import Following from './activitypub/following'; import Featured from './activitypub/featured'; import { inbox as processInbox } from '../queue'; import { isSelfHost } from '../misc/convert-host'; -import { Notes, Users, Emojis, UserKeypairs, NoteReactions } from '../models'; +import { Notes, Users, Emojis, NoteReactions } from '../models'; import { ILocalUser, User } from '../models/entities/user'; import { In } from 'typeorm'; import { renderLike } from '../remote/activitypub/renderer/like'; +import { getUserKeypair } from '../misc/keypair-store'; // Init router const router = new Router(); @@ -135,7 +136,7 @@ router.get('/users/:user/publickey', async ctx => { return; } - const keypair = await UserKeypairs.findOneOrFail(user.id); + const keypair = await getUserKeypair(user.id); if (Users.isLocalUser(user)) { ctx.body = renderActivity(renderKey(user, keypair)); -- cgit v1.2.3-freya From 0e3e90f94f684ab0dcd8039dd2087241b85ac612 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 22 Mar 2021 15:16:45 +0900 Subject: refactor --- src/misc/keypair-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/misc') diff --git a/src/misc/keypair-store.ts b/src/misc/keypair-store.ts index 43f451110c..c78fdd7555 100644 --- a/src/misc/keypair-store.ts +++ b/src/misc/keypair-store.ts @@ -6,5 +6,5 @@ import { Cache } from './cache'; const cache = new Cache(Infinity); export async function getUserKeypair(userId: User['id']): Promise { - return await cache.fetch(userId, async () => await UserKeypairs.findOneOrFail(userId)); + return await cache.fetch(userId, () => UserKeypairs.findOneOrFail(userId)); } -- cgit v1.2.3-freya