split logs so that replay works for long games

This commit is contained in:
Zutatensuppe 2021-06-05 17:13:17 +02:00
parent 22f5ce0065
commit 514b3c6b22
10 changed files with 171 additions and 102 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>🧩 jigsaw.hyottoko.club</title> <title>🧩 jigsaw.hyottoko.club</title>
<script type="module" crossorigin src="/assets/index.7efa4c6c.js"></script> <script type="module" crossorigin src="/assets/index.ab1d6e0f.js"></script>
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js"> <link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
<link rel="stylesheet" href="/assets/index.8f0efd0f.css"> <link rel="stylesheet" href="/assets/index.8f0efd0f.css">
</head> </head>

View file

@ -3,8 +3,6 @@ import express from 'express';
import compression from 'compression'; import compression from 'compression';
import multer from 'multer'; import multer from 'multer';
import fs from 'fs'; import fs from 'fs';
import readline from 'readline';
import stream from 'stream';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
import sizeOf from 'image-size'; import sizeOf from 'image-size';
@ -1249,6 +1247,7 @@ const PUBLIC_DIR = `${BASE_DIR}/build/public/`;
const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`; const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`;
const DB_FILE = `${BASE_DIR}/data/db.sqlite`; const DB_FILE = `${BASE_DIR}/data/db.sqlite`;
const LINES_PER_LOG_FILE = 10000;
const POST_GAME_LOG_DURATION = 5 * Time.MIN; const POST_GAME_LOG_DURATION = 5 * Time.MIN;
const shouldLog = (finishTs, currentTs) => { const shouldLog = (finishTs, currentTs) => {
// when not finished yet, always log // when not finished yet, always log
@ -1260,54 +1259,52 @@ const shouldLog = (finishTs, currentTs) => {
const timeSinceGameEnd = currentTs - finishTs; const timeSinceGameEnd = currentTs - finishTs;
return timeSinceGameEnd <= POST_GAME_LOG_DURATION; return timeSinceGameEnd <= POST_GAME_LOG_DURATION;
}; };
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`; const filename = (gameId, offset) => `${DATA_DIR}/log_${gameId}-${offset}.log`;
const idxname = (gameId) => `${DATA_DIR}/log_${gameId}.idx.log`;
const create = (gameId) => { const create = (gameId) => {
const file = filename(gameId); const idxfile = idxname(gameId);
if (!fs.existsSync(file)) { if (!fs.existsSync(idxfile)) {
fs.appendFileSync(file, ''); const logfile = filename(gameId, 0);
fs.appendFileSync(logfile, "");
fs.appendFileSync(idxfile, JSON.stringify({
total: 0,
currentFile: logfile,
perFile: LINES_PER_LOG_FILE,
}));
} }
}; };
const exists = (gameId) => { const exists = (gameId) => {
const file = filename(gameId); const idxfile = idxname(gameId);
return fs.existsSync(file); return fs.existsSync(idxfile);
}; };
const _log = (gameId, ...args) => { const _log = (gameId, ...args) => {
const file = filename(gameId); const idxfile = idxname(gameId);
if (!fs.existsSync(file)) { if (!fs.existsSync(idxfile)) {
return; return;
} }
const str = JSON.stringify(args); const idx = JSON.parse(fs.readFileSync(idxfile, 'utf-8'));
fs.appendFileSync(file, str + "\n"); idx.total++;
fs.appendFileSync(idx.currentFile, JSON.stringify(args) + "\n");
// prepare next log file
if (idx.total % idx.perFile === 0) {
const logfile = filename(gameId, idx.total);
fs.appendFileSync(logfile, "");
idx.currentFile = logfile;
}
fs.writeFileSync(idxfile, JSON.stringify(idx));
}; };
const get = async (gameId, offset = 0, size = 10000) => { const get = (gameId, offset = 0) => {
const file = filename(gameId); const idxfile = idxname(gameId);
if (!fs.existsSync(idxfile)) {
return [];
}
const file = filename(gameId, offset);
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
return []; return [];
} }
return new Promise((resolve) => { const log = fs.readFileSync(file, 'utf-8').split("\n");
const instream = fs.createReadStream(file); return log.map(line => {
const outstream = new stream.Writable(); return JSON.parse(line);
const rl = readline.createInterface(instream, outstream);
const lines = [];
let i = -1;
rl.on('line', (line) => {
if (!line) {
// skip empty
return;
}
i++;
if (offset > i) {
return;
}
if (offset + size <= i) {
rl.close();
return;
}
lines.push(JSON.parse(line));
});
rl.on('close', () => {
resolve(lines);
});
}); });
}; };
var GameLog = { var GameLog = {
@ -2061,7 +2058,7 @@ app.get('/api/replay-data', async (req, res) => {
res.status(404).send({ reason: 'no log found' }); res.status(404).send({ reason: 'no log found' });
return; return;
} }
const log = await GameLog.get(gameId, offset, size); const log = GameLog.get(gameId, offset);
let game = null; let game = null;
if (offset === 0) { if (offset === 0) {
// also need the game // also need the game

72
scripts/split_logs.ts Normal file
View file

@ -0,0 +1,72 @@
import fs from 'fs'
import readline from 'readline'
import stream from 'stream'
import { logger } from '../src/common/Util'
import { DATA_DIR } from '../src/server/Dirs'
const log = logger('rewrite_logs')
const doit = (file: string): Promise<void> => {
const filename = (offset: number) => file.replace(/\.log$/, `-${offset}.log`)
const idxname = () => file.replace(/\.log$/, `.idx.log`)
let perfile = 10000
const idx = {
total: 0,
currentFile: '',
perFile: perfile,
}
return new Promise((resolve) => {
const instream = fs.createReadStream(DATA_DIR + '/' + file)
const outstream = new stream.Writable()
const rl = readline.createInterface(instream, outstream)
let lines: any[] = []
let offset = 0
let count = 0
rl.on('line', (line) => {
if (!line) {
// skip empty
return
}
count++
lines.push(line)
if (count >= perfile) {
const fn = filename(offset)
idx.currentFile = fn
idx.total += count
fs.writeFileSync(DATA_DIR + '/' + fn, lines.join("\n"))
count = 0
offset += perfile
lines = []
}
})
rl.on('close', () => {
if (count > 0) {
const fn = filename(offset)
idx.currentFile = fn
idx.total += count
fs.writeFileSync(DATA_DIR + '/' + fn, lines.join("\n"))
count = 0
offset += perfile
lines = []
}
fs.writeFileSync(DATA_DIR + '/' + idxname(), JSON.stringify(idx))
resolve()
})
})
}
let logs = fs.readdirSync(DATA_DIR)
.filter(f => f.toLowerCase().match(/^log_.*\.log$/))
;(async () => {
for (const file of logs) {
await doit(file)
}
})()

View file

@ -114,10 +114,9 @@ function connect(
async function requestReplayData( async function requestReplayData(
gameId: string, gameId: string,
offset: number, offset: number
size: number
): Promise<ReplayData> { ): Promise<ReplayData> {
const args = { gameId, offset, size } const args = { gameId, offset }
const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`) const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`)
const json: ReplayData = await res.json() const json: ReplayData = await res.json()
return json return json

View file

@ -69,7 +69,6 @@ interface Replay {
skipNonActionPhases: boolean skipNonActionPhases: boolean
// //
dataOffset: number dataOffset: number
dataSize: number
} }
const shouldDrawPiece = (piece: Piece) => { const shouldDrawPiece = (piece: Piece) => {
@ -301,9 +300,8 @@ export async function main(
lastRealTs: 0, lastRealTs: 0,
lastGameTs: 0, lastGameTs: 0,
gameStartTs: 0, gameStartTs: 0,
skipNonActionPhases: false, skipNonActionPhases: true,
dataOffset: 0, dataOffset: 0,
dataSize: 10000,
} }
Communication.onConnectionStateChange((state) => { Communication.onConnectionStateChange((state) => {
@ -314,11 +312,10 @@ export async function main(
gameId: string gameId: string
): Promise<ReplayData> => { ): Promise<ReplayData> => {
const offset = REPLAY.dataOffset const offset = REPLAY.dataOffset
REPLAY.dataOffset += REPLAY.dataSize REPLAY.dataOffset += 10000 // meh
const replay: ReplayData = await Communication.requestReplayData( const replay: ReplayData = await Communication.requestReplayData(
gameId, gameId,
offset, offset
REPLAY.dataSize
) )
// cut log that was already handled // cut log that was already handled
@ -326,7 +323,7 @@ export async function main(
REPLAY.logPointer = 0 REPLAY.logPointer = 0
REPLAY.log.push(...replay.log) REPLAY.log.push(...replay.log)
if (replay.log.length < REPLAY.dataSize) { if (replay.log.length === 0) {
REPLAY.final = true REPLAY.final = true
} }
return replay return replay
@ -340,10 +337,6 @@ export async function main(
Game.setGame(gameObject.id, gameObject) Game.setGame(gameObject.id, gameObject)
TIME = () => Time.timestamp() TIME = () => Time.timestamp()
} else if (MODE === MODE_REPLAY) { } else if (MODE === MODE_REPLAY) {
REPLAY.logPointer = 0
REPLAY.dataSize = 10000
REPLAY.speeds = [0.5, 1, 2, 5, 10, 20, 50, 100, 250, 500]
REPLAY.speedIdx = 1
const replay: ReplayData = await queryNextReplayBatch(gameId) const replay: ReplayData = await queryNextReplayBatch(gameId)
if (!replay.game) { if (!replay.game) {
throw '[ 2021-05-29 no game received ]' throw '[ 2021-05-29 no game received ]'
@ -354,8 +347,6 @@ export async function main(
REPLAY.lastRealTs = Time.timestamp() REPLAY.lastRealTs = Time.timestamp()
REPLAY.gameStartTs = parseInt(replay.log[0][4], 10) REPLAY.gameStartTs = parseInt(replay.log[0][4], 10)
REPLAY.lastGameTs = REPLAY.gameStartTs REPLAY.lastGameTs = REPLAY.gameStartTs
REPLAY.paused = false
REPLAY.skipNonActionPhases = false
TIME = () => REPLAY.lastGameTs TIME = () => REPLAY.lastGameTs
} else { } else {
@ -493,6 +484,10 @@ export async function main(
doSetSpeedStatus() doSetSpeedStatus()
} }
const replayOnSkipToggle = () => {
REPLAY.skipNonActionPhases = !REPLAY.skipNonActionPhases
}
const intervals: NodeJS.Timeout[] = [] const intervals: NodeJS.Timeout[] = []
let to: NodeJS.Timeout let to: NodeJS.Timeout
const clearIntervals = () => { const clearIntervals = () => {
@ -520,9 +515,6 @@ export async function main(
doSetSpeedStatus() doSetSpeedStatus()
} }
// // TODO: remove (make changable via interface)
// REPLAY.skipNonActionPhases = true
if (MODE === MODE_PLAY) { if (MODE === MODE_PLAY) {
Communication.onServerChange((msg: ServerEvent) => { Communication.onServerChange((msg: ServerEvent) => {
const msgType = msg[0] const msgType = msg[0]
@ -608,10 +600,8 @@ export async function main(
const nextTs: Timestamp = REPLAY.gameStartTs + nextLogEntry[nextLogEntry.length - 1] const nextTs: Timestamp = REPLAY.gameStartTs + nextLogEntry[nextLogEntry.length - 1]
if (nextTs > maxGameTs) { if (nextTs > maxGameTs) {
// next log entry is too far into the future // next log entry is too far into the future
if (REPLAY.skipNonActionPhases && (maxGameTs + 50 < nextTs)) { if (REPLAY.skipNonActionPhases && (maxGameTs + 500 * Time.MS < nextTs)) {
const skipInterval = nextTs - currTs const skipInterval = nextTs - currTs
// lets skip to the next log entry
// log.info('skipping non-action, from', maxGameTs, skipInterval)
maxGameTs += skipInterval maxGameTs += skipInterval
} }
break break
@ -874,6 +864,7 @@ export async function main(
replayOnSpeedUp, replayOnSpeedUp,
replayOnSpeedDown, replayOnSpeedDown,
replayOnPauseToggle, replayOnPauseToggle,
replayOnSkipToggle,
previewImageUrl, previewImageUrl,
player: { player: {
background: playerBgColor(), background: playerBgColor(),

View file

@ -12,6 +12,12 @@
> >
<div> <div>
<div>{{replayText}}</div> <div>{{replayText}}</div>
<div>
<label>Skip no action phases: <input
type="checkbox"
v-model="skipNoAction"
@change="g.replayOnSkipToggle()" /></label>
</div>
<button class="btn" @click="g.replayOnSpeedUp()"></button> <button class="btn" @click="g.replayOnSpeedUp()"></button>
<button class="btn" @click="g.replayOnSpeedDown()"></button> <button class="btn" @click="g.replayOnSpeedDown()"></button>
<button class="btn" @click="g.replayOnPauseToggle()"></button> <button class="btn" @click="g.replayOnPauseToggle()"></button>
@ -59,6 +65,7 @@ export default defineComponent({
duration: 0, duration: 0,
piecesDone: 0, piecesDone: 0,
piecesTotal: 0, piecesTotal: 0,
skipNoAction: true,
overlay: '', overlay: '',
@ -80,6 +87,7 @@ export default defineComponent({
replayOnSpeedUp: () => {}, replayOnSpeedUp: () => {},
replayOnSpeedDown: () => {}, replayOnSpeedDown: () => {},
replayOnPauseToggle: () => {}, replayOnPauseToggle: () => {},
replayOnSkipToggle: () => {},
connect: () => {}, connect: () => {},
disconnect: () => {}, disconnect: () => {},
unload: () => {}, unload: () => {},

View file

@ -8,6 +8,7 @@ import { DATA_DIR } from './../server/Dirs'
const log = logger('GameLog.js') const log = logger('GameLog.js')
const LINES_PER_LOG_FILE = 10000
const POST_GAME_LOG_DURATION = 5 * Time.MIN const POST_GAME_LOG_DURATION = 5 * Time.MIN
const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => { const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => {
@ -22,62 +23,63 @@ const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => {
return timeSinceGameEnd <= POST_GAME_LOG_DURATION return timeSinceGameEnd <= POST_GAME_LOG_DURATION
} }
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log` const filename = (gameId: string, offset: number) => `${DATA_DIR}/log_${gameId}-${offset}.log`
const idxname = (gameId: string) => `${DATA_DIR}/log_${gameId}.idx.log`
const create = (gameId: string): void => { const create = (gameId: string): void => {
const file = filename(gameId) const idxfile = idxname(gameId)
if (!fs.existsSync(file)) { if (!fs.existsSync(idxfile)) {
fs.appendFileSync(file, '') const logfile = filename(gameId, 0)
fs.appendFileSync(logfile, "")
fs.appendFileSync(idxfile, JSON.stringify({
total: 0,
currentFile: logfile,
perFile: LINES_PER_LOG_FILE,
}))
} }
} }
const exists = (gameId: string): boolean => { const exists = (gameId: string): boolean => {
const file = filename(gameId) const idxfile = idxname(gameId)
return fs.existsSync(file) return fs.existsSync(idxfile)
} }
const _log = (gameId: string, ...args: Array<any>): void => { const _log = (gameId: string, ...args: Array<any>): void => {
const file = filename(gameId) const idxfile = idxname(gameId)
if (!fs.existsSync(file)) { if (!fs.existsSync(idxfile)) {
return return
} }
const str = JSON.stringify(args)
fs.appendFileSync(file, str + "\n") const idx = JSON.parse(fs.readFileSync(idxfile, 'utf-8'))
idx.total++
fs.appendFileSync(idx.currentFile, JSON.stringify(args) + "\n")
// prepare next log file
if (idx.total % idx.perFile === 0) {
const logfile = filename(gameId, idx.total)
fs.appendFileSync(logfile, "")
idx.currentFile = logfile
}
fs.writeFileSync(idxfile, JSON.stringify(idx))
} }
const get = async ( const get = (
gameId: string, gameId: string,
offset: number = 0, offset: number = 0,
size: number = 10000 ): any[] => {
): Promise<any[]> => { const idxfile = idxname(gameId)
const file = filename(gameId) if (!fs.existsSync(idxfile)) {
return []
}
const file = filename(gameId, offset)
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
return [] return []
} }
return new Promise((resolve) => {
const instream = fs.createReadStream(file) const log = fs.readFileSync(file, 'utf-8').split("\n")
const outstream = new stream.Writable() return log.map(line => {
const rl = readline.createInterface(instream, outstream) return JSON.parse(line)
const lines: any[] = []
let i = -1
rl.on('line', (line) => {
if (!line) {
// skip empty
return
}
i++
if (offset > i) {
return
}
if (offset + size <= i) {
rl.close()
return
}
lines.push(JSON.parse(line))
})
rl.on('close', () => {
resolve(lines)
})
}) })
} }

View file

@ -80,7 +80,7 @@ app.get('/api/replay-data', async (req, res): Promise<void> => {
res.status(404).send({ reason: 'no log found' }) res.status(404).send({ reason: 'no log found' })
return return
} }
const log = await GameLog.get(gameId, offset, size) const log = GameLog.get(gameId, offset)
let game: GameType|null = null let game: GameType|null = null
if (offset === 0) { if (offset === 0) {
// also need the game // also need the game