This commit is contained in:
syuilo 2018-06-09 04:14:26 +09:00
parent 38c57030f8
commit 8bb5dab657
20 changed files with 333 additions and 18 deletions

View file

@ -63,6 +63,7 @@ common:
memo: "メモ"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
@ -249,6 +250,10 @@ common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View file

@ -152,7 +152,7 @@
"mkdirp": "0.5.1",
"mocha": "5.2.0",
"moji": "0.5.1",
"mongodb": "3.0.8",
"mongodb": "3.0.10",
"monk": "6.0.6",
"ms": "2.1.1",
"nan": "2.10.0",

View file

@ -3,15 +3,15 @@ import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Server stream connection
* Notes stats stream connection
*/
export class ServerStream extends Stream {
export class NotesStatsStream extends Stream {
constructor(os: MiOS) {
super(os, 'server');
super(os, 'notes-stats');
}
}
export class ServerStreamManager extends StreamManager<ServerStream> {
export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
@ -22,7 +22,7 @@ export class ServerStreamManager extends StreamManager<ServerStream> {
public getConnection() {
if (this.connection == null) {
this.connection = new ServerStream(this.os);
this.connection = new NotesStatsStream(this.os);
}
return this.connection;

View file

@ -0,0 +1,30 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Server stats stream connection
*/
export class ServerStatsStream extends Stream {
constructor(os: MiOS) {
super(os, 'server-stats');
}
}
export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
super();
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new ServerStatsStream(this.os);
}
return this.connection;
}
}

View file

@ -4,6 +4,7 @@ import wAnalogClock from './analog-clock.vue';
import wVersion from './version.vue';
import wRss from './rss.vue';
import wServer from './server.vue';
import wPostsMonitor from './posts-monitor.vue';
import wMemo from './memo.vue';
import wBroadcast from './broadcast.vue';
import wCalendar from './calendar.vue';
@ -22,6 +23,7 @@ Vue.component('mkw-tips', wTips);
Vue.component('mkw-donation', wDonation);
Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-server', wServer);
Vue.component('mkw-posts-monitor', wPostsMonitor);
Vue.component('mkw-memo', wMemo);
Vue.component('mkw-rss', wRss);
Vue.component('mkw-version', wVersion);

View file

@ -0,0 +1,182 @@
<template>
<div class="mkw-posts-monitor">
<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
<template slot="header">%fa:chart-line%%i18n:@title%</template>
<button slot="func" @click="toggle" title="%i18n:@toggle%">%fa:sort%</button>
<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }" :data-darkmode="$store.state.device.darkmode">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" v-show="props.view != 2">
<defs>
<linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polyline
:points="fediPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"/>
</mask>
</defs>
<rect
x="-1" y="-1"
:width="viewBoxX + 2" :height="viewBoxY + 2"
:style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/>
<text x="1" y="5">Fedi</text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" v-show="props.view != 1">
<defs>
<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polyline
:points="localPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"/>
</mask>
</defs>
<rect
x="-1" y="-1"
:width="viewBoxX + 2" :height="viewBoxY + 2"
:style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/>
<text x="1" y="5">Local</text>
</svg>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import * as uuid from 'uuid';
export default define({
name: 'server',
props: () => ({
design: 0,
view: 0
})
}).extend({
data() {
return {
connection: null,
connectionId: null,
viewBoxY: 30,
stats: [],
fediGradientId: uuid(),
fediMaskId: uuid(),
localGradientId: uuid(),
localMaskId: uuid(),
fediPolylinePoints: '',
localPolylinePoints: '',
fediPolygonPoints: '',
localPolygonPoints: ''
};
},
computed: {
viewBoxX(): number {
return this.props.view == 0 ? 50 : 100;
}
},
watch: {
viewBoxX() {
this.draw();
}
},
mounted() {
this.connection = (this as any).os.streams.notesStatsStream.getConnection();
this.connectionId = (this as any).os.streams.notesStatsStream.use();
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send({
type: 'requestLog',
id: Math.random().toString()
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
(this as any).os.streams.notesStatsStream.dispose(this.connectionId);
},
methods: {
toggle() {
if (this.props.view == 2) {
this.props.view = 0;
} else {
this.props.view++;
}
this.save();
},
func() {
if (this.props.design == 2) {
this.props.design = 0;
} else {
this.props.design++;
}
this.save();
},
draw() {
const stats = this.props.view == 0 ? this.stats.slice(0, 50) : this.stats;
const fediPeak = Math.max.apply(null, this.stats.map(x => x.all)) || 1;
const localPeak = Math.max.apply(null, this.stats.map(x => x.local)) || 1;
this.fediPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.all / fediPeak)) * this.viewBoxY}`).join(' ');
this.localPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.local / localPeak)) * this.viewBoxY}`).join(' ');
this.fediPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.localPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
},
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
this.draw();
},
onStatsLog(statsLog) {
statsLog.forEach(stats => this.onStats(stats));
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
&.dual
> svg
width 50%
float left
&:first-child
padding-right 5px
&:last-child
padding-left 5px
> svg
display block
padding 10px
width 100%
> text
font-size 5px
fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
> tspan
opacity 0.5
&:after
content ""
display block
clear both
.qpdmibaztplkylerhdbllwcokyrfxeyj[data-darkmode]
root(true)
.qpdmibaztplkylerhdbllwcokyrfxeyj:not([data-darkmode])
root(false)
</style>

View file

@ -55,11 +55,11 @@ export default define({
this.fetching = false;
});
this.connection = (this as any).os.streams.serverStream.getConnection();
this.connectionId = (this as any).os.streams.serverStream.use();
this.connection = (this as any).os.streams.serverStatsStream.getConnection();
this.connectionId = (this as any).os.streams.serverStatsStream.use();
},
beforeDestroy() {
(this as any).os.streams.serverStream.dispose(this.connectionId);
(this as any).os.streams.serverStatsStream.dispose(this.connectionId);
},
methods: {
toggle() {

View file

@ -23,6 +23,7 @@
<option value="post-form">%i18n:common.widgets.post-form%</option>
<option value="messaging">%i18n:common.widgets.messaging%</option>
<option value="memo">%i18n:common.widgets.memo%</option>
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="donation">%i18n:common.widgets.donation%</option>
<option value="nav">%i18n:common.widgets.nav%</option>

View file

@ -23,6 +23,7 @@
<option value="post-form">%i18n:common.widgets.post-form%</option>
<option value="messaging">%i18n:common.widgets.messaging%</option>
<option value="memo">%i18n:common.widgets.memo%</option>
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="donation">%i18n:common.widgets.donation%</option>
<option value="nav">%i18n:common.widgets.nav%</option>

View file

@ -8,7 +8,8 @@ import Progress from './common/scripts/loading';
import Connection from './common/scripts/streaming/stream';
import { HomeStreamManager } from './common/scripts/streaming/home';
import { DriveStreamManager } from './common/scripts/streaming/drive';
import { ServerStreamManager } from './common/scripts/streaming/server';
import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats';
import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats';
import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index';
import { OthelloStreamManager } from './common/scripts/streaming/othello';
@ -104,14 +105,16 @@ export default class MiOS extends EventEmitter {
localTimelineStream: LocalTimelineStreamManager;
globalTimelineStream: GlobalTimelineStreamManager;
driveStream: DriveStreamManager;
serverStream: ServerStreamManager;
serverStatsStream: ServerStatsStreamManager;
notesStatsStream: NotesStatsStreamManager;
messagingIndexStream: MessagingIndexStreamManager;
othelloStream: OthelloStreamManager;
} = {
localTimelineStream: null,
globalTimelineStream: null,
driveStream: null,
serverStream: null,
serverStatsStream: null,
notesStatsStream: null,
messagingIndexStream: null,
othelloStream: null
};
@ -218,7 +221,8 @@ export default class MiOS extends EventEmitter {
this.store = initStore(this);
//#region Init stream managers
this.streams.serverStream = new ServerStreamManager(this);
this.streams.serverStatsStream = new ServerStatsStreamManager(this);
this.streams.notesStatsStream = new NotesStatsStreamManager(this);
this.once('signedin', () => {
// Init home stream manager

View file

@ -15,6 +15,7 @@
<option value="rss">%i18n:common.widgets.rss%</option>
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
<option value="slideshow">%i18n:common.widgets.slideshow%</option>
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
<option value="version">%i18n:common.widgets.version%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="memo">%i18n:common.widgets.memo%</option>

View file

@ -12,7 +12,10 @@ const uri = u && p
*/
import mongo from 'monk';
const db = mongo(uri);
const db = mongo(uri, {
poolSize: 16,
keepAlive: 1
});
export default db;

View file

@ -18,6 +18,7 @@ import EnvironmentInfo from './utils/environmentInfo';
import MachineInfo from './utils/machineInfo';
import DependencyInfo from './utils/dependencyInfo';
import serverStats from './server-stats';
import notesStats from './notes-stats';
import loadConfig from './config/load';
import { Config } from './config/types';
@ -50,6 +51,7 @@ function main() {
ev.mount();
serverStats();
notesStats();
} else {
workerMain(opt);
}

View file

@ -16,6 +16,9 @@ import Following from './following';
const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId');
Note.createIndex({
createdAt: -1
});
export default Note;
export function isValidText(text: string): boolean {

20
src/notes-stats-child.ts Normal file
View file

@ -0,0 +1,20 @@
import Note from './models/note';
setInterval(async () => {
const [all, local] = await Promise.all([Note.count({
createdAt: {
$gte: new Date(Date.now() - 3000)
}
}), Note.count({
createdAt: {
$gte: new Date(Date.now() - 3000)
},
'_user.host': null
})]);
const stats = {
all, local
};
process.send(stats);
}, 3000);

20
src/notes-stats.ts Normal file
View file

@ -0,0 +1,20 @@
import * as childProcess from 'child_process';
import Xev from 'xev';
const ev = new Xev();
export default function() {
const log = [];
const p = childProcess.fork(__dirname + '/notes-stats-child.js');
p.on('message', stats => {
ev.emit('notesStats', stats);
log.push(stats);
if (log.length > 100) log.shift();
});
ev.on('requestNotesStatsLog', id => {
ev.emit('notesStatsLog:' + id, log);
});
}

View file

@ -6,7 +6,7 @@ import Xev from 'xev';
const ev = new Xev();
/**
* Report stats regularly
* Report server stats regularly
*/
export default function() {
const log = [];

View file

@ -0,0 +1,35 @@
import * as websocket from 'websocket';
import Xev from 'xev';
const ev = new Xev();
export default function(request: websocket.request, connection: websocket.connection): void {
const onStats = stats => {
connection.send(JSON.stringify({
type: 'stats',
body: stats
}));
};
connection.on('message', async data => {
const msg = JSON.parse(data.utf8Data);
switch (msg.type) {
case 'requestLog':
ev.once('notesStatsLog:' + msg.id, statsLog => {
connection.send(JSON.stringify({
type: 'statsLog',
body: statsLog
}));
});
ev.emit('requestNotesStatsLog', msg.id);
break;
}
});
ev.addListener('notesStats', onStats);
connection.on('close', () => {
ev.removeListener('notesStats', onStats);
});
}

View file

@ -12,7 +12,8 @@ import messagingStream from './stream/messaging';
import messagingIndexStream from './stream/messaging-index';
import othelloGameStream from './stream/othello-game';
import othelloStream from './stream/othello';
import serverStream from './stream/server';
import serverStatsStream from './stream/server-stats';
import notesStatsStream from './stream/notes-stats';
import requestsStream from './stream/requests';
import { ParsedUrlQuery } from 'querystring';
import authenticate from './authenticate';
@ -28,8 +29,13 @@ module.exports = (server: http.Server) => {
ws.on('request', async (request) => {
const connection = request.accept();
if (request.resourceURL.pathname === '/server') {
serverStream(request, connection);
if (request.resourceURL.pathname === '/server-stats') {
serverStatsStream(request, connection);
return;
}
if (request.resourceURL.pathname === '/notes-stats') {
notesStatsStream(request, connection);
return;
}