Refactoring

This commit is contained in:
syuilo 2018-10-21 07:10:35 +09:00
parent 55f98b802b
commit 7fdd4d1cdd
12 changed files with 667 additions and 764 deletions

View File

@ -90,11 +90,25 @@ export default Vue.extend({
},
stats(): any[] {
return (
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
const stats =
this.span == 'day' ? this.chart.perDay :
this.span == 'hour' ? this.chart.perHour :
null
);
null;
stats.forEach((s, i) => {
s.date =
this.span == 'day' ? new Date(y, m, d - i) :
this.span == 'hour' ? new Date(y, m, d, h - i) :
null;
});
return stats;
}
},
@ -560,19 +574,19 @@ export default Vue.extend({
networkRequestsChart(): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
requests: x.network.requests
incoming: x.network.incomingRequests
}));
return [{
datasets: [{
label: 'Requests',
label: 'Incoming',
fill: true,
backgroundColor: rgba(colors.localPlus),
borderColor: colors.localPlus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.requests }))
data: data.map(x => ({ t: x.date, y: x.incomingRequests }))
}]
}];
},

View File

@ -1,228 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const Stats = db.get<IStats>('stats');
Stats.createIndex({ span: -1, date: -1 }, { unique: true });
export default Stats;
export interface IStats {
_id: mongo.ObjectID;
/**
*
*/
date: Date;
/**
*
*/
span: 'day' | 'hour';
/**
*
*/
users: {
local: {
/**
* ()
*/
total: number;
/**
* ()
*/
inc: number;
/**
* ()
*/
dec: number;
};
remote: {
/**
* ()
*/
total: number;
/**
* ()
*/
inc: number;
/**
* ()
*/
dec: number;
};
};
/**
* 稿
*/
notes: {
local: {
/**
* 稿 ()
*/
total: number;
/**
* 稿 ()
*/
inc: number;
/**
* 稿 ()
*/
dec: number;
diffs: {
/**
* 稿 ()
*/
normal: number;
/**
* 稿 ()
*/
reply: number;
/**
* Renoteの投稿数の差分 ()
*/
renote: number;
};
};
remote: {
/**
* 稿 ()
*/
total: number;
/**
* 稿 ()
*/
inc: number;
/**
* 稿 ()
*/
dec: number;
diffs: {
/**
* 稿 ()
*/
normal: number;
/**
* 稿 ()
*/
reply: number;
/**
* Renoteの投稿数の差分 ()
*/
renote: number;
};
};
};
/**
* ()
*/
drive: {
local: {
/**
* ()
*/
totalCount: number;
/**
* ()
*/
totalSize: number;
/**
* ()
*/
incCount: number;
/**
* 使 ()
*/
incSize: number;
/**
* ()
*/
decCount: number;
/**
* 使 ()
*/
decSize: number;
};
remote: {
/**
* ()
*/
totalCount: number;
/**
* ()
*/
totalSize: number;
/**
* ()
*/
incCount: number;
/**
* 使 ()
*/
incSize: number;
/**
* ()
*/
decCount: number;
/**
* 使 ()
*/
decSize: number;
};
};
/**
*
*/
network: {
/**
*
*/
requests: number;
/**
*
* TIP: (totalTime / requests)
*/
totalTime: number;
/**
*
*/
incomingBytes: number;
/**
*
*/
outgoingBytes: number;
};
}

View File

@ -10,7 +10,7 @@ import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'
import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm';
import { updateUserStats } from '../../../services/update-chart';
import { coreChart } from '../../../services/stats';
import { URL } from 'url';
import { resolveNote } from './note';
@ -180,7 +180,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
}
}, { upsert: true });
updateUserStats(user, true);
coreChart.updateUserStats(user, true);
//#endregion
//#region アイコンとヘッダー画像をフェッチ

View File

@ -1,58 +1,6 @@
import $ from 'cafy';
import Stats, { IStats } from '../../../models/stats';
import getParams from '../get-params';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
function migrateStats(stats: IStats[]) {
stats.forEach(stat => {
if (stat.network == null) {
stat.network = {
requests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
};
}
const isOldData =
stat.users.local.inc == null ||
stat.users.local.dec == null ||
stat.users.remote.inc == null ||
stat.users.remote.dec == null ||
stat.notes.local.inc == null ||
stat.notes.local.dec == null ||
stat.notes.remote.inc == null ||
stat.notes.remote.dec == null ||
stat.drive.local.incCount == null ||
stat.drive.local.decCount == null ||
stat.drive.local.incSize == null ||
stat.drive.local.decSize == null ||
stat.drive.remote.incCount == null ||
stat.drive.remote.decCount == null ||
stat.drive.remote.incSize == null ||
stat.drive.remote.decSize == null;
if (!isOldData) return;
stat.users.local.inc = (stat as any).users.local.diff;
stat.users.local.dec = 0;
stat.users.remote.inc = (stat as any).users.remote.diff;
stat.users.remote.dec = 0;
stat.notes.local.inc = (stat as any).notes.local.diff;
stat.notes.local.dec = 0;
stat.notes.remote.inc = (stat as any).notes.remote.diff;
stat.notes.remote.dec = 0;
stat.drive.local.incCount = (stat as any).drive.local.diffCount;
stat.drive.local.decCount = 0;
stat.drive.local.incSize = (stat as any).drive.local.diffSize;
stat.drive.local.decSize = 0;
stat.drive.remote.incCount = (stat as any).drive.remote.diffCount;
stat.drive.remote.decCount = 0;
stat.drive.remote.incSize = (stat as any).drive.remote.diffSize;
stat.drive.remote.decSize = 0;
});
}
import { coreChart } from '../../../services/stats';
export const meta = {
desc: {
@ -73,205 +21,13 @@ export default (params: any) => new Promise(async (res, rej) => {
const [ps, psErr] = getParams(meta, params);
if (psErr) throw psErr;
const daysRange = ps.limit;
const hoursRange = ps.limit;
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
const [statsPerDay, statsPerHour] = await Promise.all([
Stats.find({
span: 'day',
date: {
$gt: new Date(y, m, d - daysRange)
}
}, {
sort: {
date: -1
},
fields: {
_id: 0
}
}),
Stats.find({
span: 'hour',
date: {
$gt: new Date(y, m, d, h - hoursRange)
}
}, {
sort: {
date: -1
},
fields: {
_id: 0
}
}),
coreChart.getStats('day', ps.limit),
coreChart.getStats('hour', ps.limit)
]);
// 後方互換性のため
migrateStats(statsPerDay);
migrateStats(statsPerHour);
const format = (src: IStats[], span: 'day' | 'hour') => {
const chart: Array<Omit<Omit<IStats, '_id'>, 'span'>> = [];
const range =
span == 'day' ? daysRange :
span == 'hour' ? hoursRange :
null;
for (let i = (range - 1); i >= 0; i--) {
const current =
span == 'day' ? new Date(y, m, d - i) :
span == 'hour' ? new Date(y, m, d, h - i) :
null;
const stat = src.find(s => s.date.getTime() == current.getTime());
if (stat) {
chart.unshift(stat);
} else { // 隙間埋め
const mostRecent = src.find(s => s.date.getTime() < current.getTime());
if (mostRecent) {
chart.unshift({
date: current,
users: {
local: {
total: mostRecent.users.local.total,
inc: 0,
dec: 0
},
remote: {
total: mostRecent.users.remote.total,
inc: 0,
dec: 0
}
},
notes: {
local: {
total: mostRecent.notes.local.total,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: mostRecent.notes.remote.total,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: mostRecent.drive.local.totalCount,
totalSize: mostRecent.drive.local.totalSize,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
},
remote: {
totalCount: mostRecent.drive.remote.totalCount,
totalSize: mostRecent.drive.remote.totalSize,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
}
},
network: {
requests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
}
});
} else {
chart.unshift({
date: current,
users: {
local: {
total: 0,
inc: 0,
dec: 0
},
remote: {
total: 0,
inc: 0,
dec: 0
}
},
notes: {
local: {
total: 0,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: 0,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: 0,
totalSize: 0,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
},
remote: {
totalCount: 0,
totalSize: 0,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
}
},
network: {
requests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
}
});
}
}
}
chart.forEach(x => {
delete (x as any).span;
});
return chart;
};
res({
perDay: format(statsPerDay, 'day'),
perHour: format(statsPerHour, 'hour')
perDay: statsPerDay,
perHour: statsPerHour
});
});

View File

@ -7,7 +7,7 @@ import generateUserToken from '../common/generate-native-user-token';
import config from '../../../config';
import Meta from '../../../models/meta';
import RegistrationTicket from '../../../models/registration-tickets';
import { updateUserStats } from '../../../services/update-chart';
import { coreChart } from '../../../services/stats';
if (config.recaptcha) {
recaptcha.init({
@ -130,7 +130,7 @@ export default async (ctx: Koa.Context) => {
}, { upsert: true });
//#endregion
updateUserStats(account, true);
coreChart.updateUserStats(account, true);
const res = await pack(account, account, {
detail: true,

View File

@ -17,7 +17,7 @@ const requestStats = require('request-stats');
import activityPub from './activitypub';
import webFinger from './webfinger';
import config from '../config';
import { updateNetworkStats } from '../services/update-chart';
import { coreChart } from '../services/stats';
import apiServer from './api';
// Init app
@ -104,7 +104,7 @@ export default () => new Promise(resolve => {
const outgoingBytes = queue.reduce((a, b) => a + b.res.bytes, 0);
queue = [];
updateNetworkStats(requests, time, incomingBytes, outgoingBytes);
coreChart.updateNetworkStats(requests, time, incomingBytes, outgoingBytes);
}, 5000);
//#endregion
});

View File

@ -17,7 +17,7 @@ import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import delFile from './delete-file';
import config from '../../config';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
import { updateDriveStats } from '../update-chart';
import { coreChart } from '../stats';
const log = debug('misskey:drive:add-file');
@ -389,7 +389,7 @@ export default async function(
});
// 統計を更新
updateDriveStats(driveFile, true);
coreChart.updateDriveStats(driveFile, true);
return driveFile;
}

View File

@ -2,7 +2,7 @@ import * as Minio from 'minio';
import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
import config from '../../config';
import { updateDriveStats } from '../update-chart';
import { coreChart } from '../stats';
export default async function(file: IDriveFile, isExpired = false) {
if (file.metadata.storage == 'minio') {
@ -48,5 +48,5 @@ export default async function(file: IDriveFile, isExpired = false) {
//#endregion
// 統計を更新
updateDriveStats(file, false);
coreChart.updateDriveStats(file, false);
}

View File

@ -23,7 +23,7 @@ import registerHashtag from '../register-hashtag';
import isQuote from '../../misc/is-quote';
import { TextElementMention } from '../../mfm/parse/elements/mention';
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
import { updateNoteStats } from '../update-chart';
import { coreChart } from '../stats';
import { erase, unique } from '../../prelude/array';
import insertNoteUnread from './unread';
@ -165,7 +165,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
}
// 統計を更新
updateNoteStats(note, true);
coreChart.updateNoteStats(note, true);
// ハッシュタグ登録
tags.map(tag => registerHashtag(user, tag));

View File

@ -6,7 +6,7 @@ import pack from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import Following from '../../models/following';
import renderTombstone from '../../remote/activitypub/renderer/tombstone';
import { updateNoteStats } from '../update-chart';
import { coreChart } from '../stats';
import config from '../../config';
import NoteUnread from '../../models/note-unread';
import read from './read';
@ -63,5 +63,5 @@ export default async function(user: IUser, note: INote) {
//#endregion
// 統計を更新
updateNoteStats(note, false);
coreChart.updateNoteStats(note, false);
}

628
src/services/stats.ts Normal file
View File

@ -0,0 +1,628 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
import { INote } from '../models/note';
import { isLocalUser, IUser } from '../models/user';
import { IDriveFile } from '../models/drive-file';
import { ICollection } from 'monk';
type Obj = { [key: string]: any };
type Partial<T> = {
[P in keyof T]?: Partial<T[P]>;
};
type Span = 'day' | 'hour';
type ChartDocument<T extends Obj> = {
_id: mongo.ObjectID;
/**
*
*/
date: Date;
/**
*
*/
span: Span;
/**
*
*/
data: T;
};
abstract class Chart<T> {
protected collection: ICollection<ChartDocument<T>>;
protected abstract generateInitialStats(): T;
protected abstract generateEmptyStats(mostRecentStats: T): T;
constructor(dbCollectionName: string) {
this.collection = db.get<ChartDocument<T>>(dbCollectionName);
this.collection.createIndex({ span: -1, date: -1 }, { unique: true });
}
protected async getCurrentStats(span: Span, group?: Obj): Promise<ChartDocument<T>> {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
const current =
span == 'day' ? new Date(y, m, d) :
span == 'hour' ? new Date(y, m, d, h) :
null;
// 現在(今日または今のHour)の統計
const currentStats = await this.collection.findOne(Object.assign({}, {
span: span,
date: current
}, group));
if (currentStats) {
return currentStats;
} else {
// 集計期間が変わってから、初めてのチャート更新なら
// 最も最近の統計を持ってくる
// * 例えば集計期間が「日」である場合で考えると、
// * 昨日何もチャートを更新するような出来事がなかった場合は、
// * 統計がそもそも作られずドキュメントが存在しないということがあり得るため、
// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
const mostRecentStats = await this.collection.findOne(Object.assign({}, {
span: span
}, group), {
sort: {
date: -1
}
});
if (mostRecentStats) {
// 現在の統計を初期挿入
const data = this.generateEmptyStats(mostRecentStats.data);
const stats = await this.collection.insert(Object.assign({}, {
span: span,
date: current,
data: data
}, group));
return stats;
} else {
// 統計が存在しなかったら
// * Misskeyインスタンスを建てて初めてのチャート更新時など
// 空の統計を作成
const data = this.generateInitialStats();
const stats = await this.collection.insert(Object.assign({}, {
span: span,
date: current,
data: data
}, group));
return stats;
}
}
}
protected update(inc: Partial<T>, group?: Obj): void {
const query: Obj = {};
const dive = (path: string, x: Obj) => {
Object.entries(x).forEach(([k, v]) => {
if (typeof v === 'number') {
query[path == null ? `data.${k}` : `data.${path}.${k}`] = v;
} else {
dive(path == null ? k : `${path}.${k}`, v);
}
});
};
dive(null, inc);
this.getCurrentStats('day', group).then(stats => {
this.collection.findOneAndUpdate({
_id: stats._id
}, {
$inc: query
});
});
this.getCurrentStats('hour', group).then(stats => {
this.collection.findOneAndUpdate({
_id: stats._id
}, {
$inc: query
});
});
}
public async getStats(span: Span, range: number, group?: Obj): Promise<T[]> {
const chart: T[] = [];
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
const gt =
span == 'day' ? new Date(y, m, d - range) :
span == 'hour' ? new Date(y, m, d, h - range) : null;
const stats = await this.collection.find(Object.assign({
span: span,
date: {
$gt: gt
}
}, group), {
sort: {
date: -1
},
fields: {
_id: 0
}
});
for (let i = (range - 1); i >= 0; i--) {
const current =
span == 'day' ? new Date(y, m, d - i) :
span == 'hour' ? new Date(y, m, d, h - i) :
null;
const stat = stats.find(s => s.date.getTime() == current.getTime());
if (stat) {
chart.unshift(stat.data);
} else { // 隙間埋め
const mostRecent = stats.find(s => s.date.getTime() < current.getTime());
if (mostRecent) {
chart.unshift(this.generateEmptyStats(mostRecent.data));
} else {
chart.unshift(this.generateInitialStats());
}
}
}
return chart;
}
}
type CoreStats = {
/**
*
*/
users: {
local: {
/**
* ()
*/
total: number;
/**
* ()
*/
inc: number;
/**
* ()
*/
dec: number;
};
remote: {
/**
* ()
*/
total: number;
/**
* ()
*/
inc: number;
/**
* ()
*/
dec: number;
};
};
/**
* 稿
*/
notes: {
local: {
/**
* 稿 ()
*/
total: number;
/**
* 稿 ()
*/
inc: number;
/**
* 稿 ()
*/
dec: number;
diffs: {
/**
* 稿 ()
*/
normal: number;
/**
* 稿 ()
*/
reply: number;
/**
* Renoteの投稿数の差分 ()
*/
renote: number;
};
};
remote: {
/**
* 稿 ()
*/
total: number;
/**
* 稿 ()
*/
inc: number;
/**
* 稿 ()
*/
dec: number;
diffs: {
/**
* 稿 ()
*/
normal: number;
/**
* 稿 ()
*/
reply: number;
/**
* Renoteの投稿数の差分 ()
*/
renote: number;
};
};
};
/**
* ()
*/
drive: {
local: {
/**
* ()
*/
totalCount: number;
/**
* ()
*/
totalSize: number;
/**
* ()
*/
incCount: number;
/**
* 使 ()
*/
incSize: number;
/**
* ()
*/
decCount: number;
/**
* 使 ()
*/
decSize: number;
};
remote: {
/**
* ()
*/
totalCount: number;
/**
* ()
*/
totalSize: number;
/**
* ()
*/
incCount: number;
/**
* 使 ()
*/
incSize: number;
/**
* ()
*/
decCount: number;
/**
* 使 ()
*/
decSize: number;
};
};
/**
*
*/
network: {
/**
*
*/
incomingRequests: number;
/**
*
*/
outgoingRequests: number;
/**
*
* TIP: (totalTime / incomingRequests)
*/
totalTime: number;
/**
*
*/
incomingBytes: number;
/**
*
*/
outgoingBytes: number;
};
};
class CoreChart extends Chart<CoreStats> {
constructor() {
super('coreStats');
}
protected generateInitialStats(): CoreStats {
return {
users: {
local: {
total: 0,
inc: 0,
dec: 0
},
remote: {
total: 0,
inc: 0,
dec: 0
}
},
notes: {
local: {
total: 0,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: 0,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: 0,
totalSize: 0,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
},
remote: {
totalCount: 0,
totalSize: 0,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
}
},
network: {
incomingRequests: 0,
outgoingRequests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
}
};
}
protected generateEmptyStats(mostRecentStats: CoreStats): CoreStats {
return {
users: {
local: {
total: mostRecentStats.users.local.total,
inc: 0,
dec: 0
},
remote: {
total: mostRecentStats.users.remote.total,
inc: 0,
dec: 0
}
},
notes: {
local: {
total: mostRecentStats.notes.local.total,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: mostRecentStats.notes.remote.total,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: mostRecentStats.drive.local.totalCount,
totalSize: mostRecentStats.drive.local.totalSize,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
},
remote: {
totalCount: mostRecentStats.drive.remote.totalCount,
totalSize: mostRecentStats.drive.remote.totalSize,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
}
},
network: {
incomingRequests: 0,
outgoingRequests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
}
};
}
public async updateUserStats(user: IUser, isAdditional: boolean) {
const origin = isLocalUser(user) ? 'local' : 'remote';
const update: Obj = {};
update.total = isAdditional ? 1 : -1;
if (isAdditional) {
update.inc = 1;
} else {
update.dec = 1;
}
const inc: Obj = {
users: {}
};
inc.users[origin] = update;
await this.update(inc);
}
public async updateNoteStats(note: INote, isAdditional: boolean) {
const origin = isLocalUser(note._user) ? 'local' : 'remote';
const update: Obj = {};
update.total = isAdditional ? 1 : -1;
if (isAdditional) {
update.inc = 1;
} else {
update.dec = 1;
}
if (note.replyId != null) {
update.diffs.reply = isAdditional ? 1 : -1;
} else if (note.renoteId != null) {
update.diffs.renote = isAdditional ? 1 : -1;
} else {
update.diffs.normal = isAdditional ? 1 : -1;
}
const inc: Obj = {
notes: {}
};
inc.notes[origin] = update;
await this.update(inc);
}
public async updateDriveStats(file: IDriveFile, isAdditional: boolean) {
const origin = isLocalUser(file.metadata._user) ? 'local' : 'remote';
const update: Obj = {};
update.totalCount = isAdditional ? 1 : -1;
update.totalSize = isAdditional ? file.length : -file.length;
if (isAdditional) {
update.incCount = 1;
update.incSize = file.length;
} else {
update.decCount = 1;
update.decSize = file.length;
}
const inc: Obj = {
drive: {}
};
inc.drive[origin] = update;
await this.update(inc);
}
public async updateNetworkStats(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
const inc: Partial<CoreStats> = {
network: {
incomingRequests: incomingRequests,
totalTime: time,
incomingBytes: incomingBytes,
outgoingBytes: outgoingBytes
}
};
await this.update(inc);
}
}
export const coreChart = new CoreChart();

View File

@ -1,267 +0,0 @@
import { INote } from '../models/note';
import Stats, { IStats } from '../models/stats';
import { isLocalUser, IUser } from '../models/user';
import { IDriveFile } from '../models/drive-file';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
async function getCurrentStats(span: 'day' | 'hour'): Promise<IStats> {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
const current =
span == 'day' ? new Date(y, m, d) :
span == 'hour' ? new Date(y, m, d, h) :
null;
// 現在(今日または今のHour)の統計
const currentStats = await Stats.findOne({
span: span,
date: current
});
if (currentStats) {
return currentStats;
} else {
// 集計期間が変わってから、初めてのチャート更新なら
// 最も最近の統計を持ってくる
// * 例えば集計期間が「日」である場合で考えると、
// * 昨日何もチャートを更新するような出来事がなかった場合は、
// * 統計がそもそも作られずドキュメントが存在しないということがあり得るため、
// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
const mostRecentStats = await Stats.findOne({
span: span
}, {
sort: {
date: -1
}
});
if (mostRecentStats) {
// 現在の統計を初期挿入
const data: Omit<IStats, '_id'> = {
span: span,
date: current,
users: {
local: {
total: mostRecentStats.users.local.total,
inc: 0,
dec: 0
},
remote: {
total: mostRecentStats.users.remote.total,
inc: 0,
dec: 0
}
},
notes: {
local: {
total: mostRecentStats.notes.local.total,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: mostRecentStats.notes.remote.total,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: mostRecentStats.drive.local.totalCount,
totalSize: mostRecentStats.drive.local.totalSize,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
},
remote: {
totalCount: mostRecentStats.drive.remote.totalCount,
totalSize: mostRecentStats.drive.remote.totalSize,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
}
},
network: {
requests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
}
};
const stats = await Stats.insert(data);
return stats;
} else {
// 統計が存在しなかったら
// * Misskeyインスタンスを建てて初めてのチャート更新時など
// 空の統計を作成
const emptyStat: Omit<IStats, '_id'> = {
span: span,
date: current,
users: {
local: {
total: 0,
inc: 0,
dec: 0
},
remote: {
total: 0,
inc: 0,
dec: 0
}
},
notes: {
local: {
total: 0,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: 0,
inc: 0,
dec: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: 0,
totalSize: 0,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
},
remote: {
totalCount: 0,
totalSize: 0,
incCount: 0,
incSize: 0,
decCount: 0,
decSize: 0
}
},
network: {
requests: 0,
totalTime: 0,
incomingBytes: 0,
outgoingBytes: 0
}
};
const stats = await Stats.insert(emptyStat);
return stats;
}
}
}
function update(inc: any) {
getCurrentStats('day').then(stats => {
Stats.findOneAndUpdate({
_id: stats._id
}, {
$inc: inc
});
});
getCurrentStats('hour').then(stats => {
Stats.findOneAndUpdate({
_id: stats._id
}, {
$inc: inc
});
});
}
export async function updateUserStats(user: IUser, isAdditional: boolean) {
const origin = isLocalUser(user) ? 'local' : 'remote';
const inc = {} as any;
inc[`users.${origin}.total`] = isAdditional ? 1 : -1;
if (isAdditional) {
inc[`users.${origin}.inc`] = 1;
} else {
inc[`users.${origin}.dec`] = 1;
}
await update(inc);
}
export async function updateNoteStats(note: INote, isAdditional: boolean) {
const origin = isLocalUser(note._user) ? 'local' : 'remote';
const inc = {} as any;
inc[`notes.${origin}.total`] = isAdditional ? 1 : -1;
if (isAdditional) {
inc[`notes.${origin}.inc`] = 1;
} else {
inc[`notes.${origin}.dec`] = 1;
}
if (note.replyId != null) {
inc[`notes.${origin}.diffs.reply`] = isAdditional ? 1 : -1;
} else if (note.renoteId != null) {
inc[`notes.${origin}.diffs.renote`] = isAdditional ? 1 : -1;
} else {
inc[`notes.${origin}.diffs.normal`] = isAdditional ? 1 : -1;
}
await update(inc);
}
export async function updateDriveStats(file: IDriveFile, isAdditional: boolean) {
const origin = isLocalUser(file.metadata._user) ? 'local' : 'remote';
const inc = {} as any;
inc[`drive.${origin}.totalCount`] = isAdditional ? 1 : -1;
inc[`drive.${origin}.totalSize`] = isAdditional ? file.length : -file.length;
if (isAdditional) {
inc[`drive.${origin}.incCount`] = 1;
inc[`drive.${origin}.incSize`] = file.length;
} else {
inc[`drive.${origin}.decCount`] = 1;
inc[`drive.${origin}.decSize`] = file.length;
}
await update(inc);
}
export async function updateNetworkStats(requests: number, time: number, incomingBytes: number, outgoingBytes: number) {
const inc = {} as any;
inc['network.requests'] = requests;
inc['network.totalTime'] = time;
inc['network.incomingBytes'] = incomingBytes;
inc['network.outgoingBytes'] = outgoingBytes;
await update(inc);
}