diff --git a/src/reader/cif/binary/parser.ts b/src/reader/cif/binary/parser.ts index 1cdbd04b1f16d02bea1c883a637f8a9bd91eb81a..fa62f7812e4ae2c793a4e3a4a5175e41b1dfb4d7 100644 --- a/src/reader/cif/binary/parser.ts +++ b/src/reader/cif/binary/parser.ts @@ -9,6 +9,7 @@ import * as Encoding from './encoding' import Field from './field' import Result from '../../result' import decodeMsgPack from '../../../utils/msgpack/decode' +import Computation from '../../../utils/computation' function checkVersions(min: number[], current: number[]) { for (let i = 0; i < 2; i++) { @@ -29,21 +30,23 @@ function Category(data: Encoding.EncodedCategory): Data.Category { } } -export default function parse(data: Uint8Array): Result<Data.File> { - const minVersion = [0, 3]; +export default function parse(data: Uint8Array) { + return new Computation<Result<Data.File>>(async ctx => { + const minVersion = [0, 3]; - try { - const unpacked = decodeMsgPack(data) as Encoding.EncodedFile; - if (!checkVersions(minVersion, unpacked.version.match(/(\d)\.(\d)\.\d/)!.slice(1).map(v => +v))) { - return Result.error<Data.File>(`Unsupported format version. Current ${unpacked.version}, required ${minVersion.join('.')}.`); + try { + const unpacked = decodeMsgPack(data) as Encoding.EncodedFile; + if (!checkVersions(minVersion, unpacked.version.match(/(\d)\.(\d)\.\d/)!.slice(1).map(v => +v))) { + return Result.error<Data.File>(`Unsupported format version. Current ${unpacked.version}, required ${minVersion.join('.')}.`); + } + const file = Data.File(unpacked.dataBlocks.map(block => { + const cats = Object.create(null); + for (const cat of block.categories) cats[cat.name] = Category(cat); + return Data.Block(cats, block.header); + })); + return Result.success(file); + } catch (e) { + return Result.error<Data.File>('' + e); } - const file = Data.File(unpacked.dataBlocks.map(block => { - const cats = Object.create(null); - for (const cat of block.categories) cats[cat.name] = Category(cat); - return Data.Block(cats, block.header); - })); - return Result.success(file); - } catch (e) { - return Result.error<Data.File>('' + e); - } + }) } \ No newline at end of file diff --git a/src/reader/cif/text/parser.ts b/src/reader/cif/text/parser.ts index 606fc02f79a4cd30f97694262715953c6b04576f..e90ef8c48544bb7614a7931e953667fa3881ce71 100644 --- a/src/reader/cif/text/parser.ts +++ b/src/reader/cif/text/parser.ts @@ -26,6 +26,7 @@ import * as Data from '../data-model' import Field from './field' import { Tokens, TokenBuilder } from '../../common/text/tokenizer' import Result from '../../result' +import Computation from '../../../utils/computation' /** * Types of supported mmCIF tokens. @@ -51,6 +52,8 @@ interface TokenizerState { currentTokenType: CifTokenType; currentTokenStart: number; currentTokenEnd: number; + + computation: Computation.Chunked } /** @@ -384,7 +387,7 @@ function moveNext(state: TokenizerState) { while (state.currentTokenType === CifTokenType.Comment) moveNextInternal(state); } -function createTokenizer(data: string): TokenizerState { +function createTokenizer(data: string, ctx: Computation.Context): TokenizerState { return { data, length: data.length, @@ -393,7 +396,8 @@ function createTokenizer(data: string): TokenizerState { currentTokenEnd: 0, currentTokenType: CifTokenType.End, currentLineNumber: 1, - isEscaped: false + isEscaped: false, + computation: new Computation.Chunked(ctx, 1000000) }; } @@ -443,10 +447,39 @@ function handleSingle(tokenizer: TokenizerState, categories: { [name: string]: D }; } +interface LoopReadState { + tokenizer: TokenizerState, + tokens: Tokens[], + fieldCount: number, + tokenCount: number +} + +function readLoopChunk(state: LoopReadState, chunkSize: number) { + const { tokenizer, tokens, fieldCount } = state; + let tokenCount = state.tokenCount; + let counter = 0; + while (tokenizer.currentTokenType === CifTokenType.Value && counter < chunkSize) { + TokenBuilder.add(tokens[(tokenCount++) % fieldCount], tokenizer.currentTokenStart, tokenizer.currentTokenEnd); + moveNext(tokenizer); + counter++; + } + state.tokenCount = tokenCount; + return tokenizer.currentTokenType === CifTokenType.Value; +} + +async function readLoopChunks(state: LoopReadState) { + const { computation } = state.tokenizer; + while (readLoopChunk(state, computation.chunkSize)) { + if (computation.requiresUpdate) { + await computation.updateProgress('Parsing...', void 0, state.tokenizer.position, state.tokenizer.data.length); + } + } +} + /** * Reads a loop. */ -function handleLoop(tokenizer: TokenizerState, categories: { [name: string]: Data.Category }): CifCategoryResult { +async function handleLoop(tokenizer: TokenizerState, categories: { [name: string]: Data.Category }): Promise<CifCategoryResult> { const loopLine = tokenizer.currentLineNumber; moveNext(tokenizer); @@ -463,13 +496,22 @@ function handleLoop(tokenizer: TokenizerState, categories: { [name: string]: Dat const fieldCount = fieldNames.length; for (let i = 0; i < fieldCount; i++) tokens[i] = TokenBuilder.create(tokenizer, rowCountEstimate); - let tokenCount = 0; - while (tokenizer.currentTokenType === CifTokenType.Value) { - TokenBuilder.add(tokens[(tokenCount++) % fieldCount], tokenizer.currentTokenStart, tokenizer.currentTokenEnd); - moveNext(tokenizer); - } + const state: LoopReadState = { + fieldCount, + tokenCount: 0, + tokenizer, + tokens + }; + + // let tokenCount = 0; + // while (tokenizer.currentTokenType === CifTokenType.Value) { + // TokenBuilder.add(tokens[(tokenCount++) % fieldCount], tokenizer.currentTokenStart, tokenizer.currentTokenEnd); + // moveNext(tokenizer); + // } - if (tokenCount % fieldCount !== 0) { + await readLoopChunks(state); + + if (state.tokenCount % fieldCount !== 0) { return { hasError: true, errorLine: tokenizer.currentLineNumber, @@ -477,7 +519,7 @@ function handleLoop(tokenizer: TokenizerState, categories: { [name: string]: Dat }; } - const rowCount = (tokenCount / fieldCount) | 0; + const rowCount = (state.tokenCount / fieldCount) | 0; const fields = Object.create(null); for (let i = 0; i < fieldCount; i++) { fields[fieldNames[i]] = Field(tokens[i], rowCount); @@ -511,9 +553,9 @@ function result(data: Data.File) { * * @returns CifParserResult wrapper of the result. */ -function parseInternal(data: string): Result<Data.File> { +async function parseInternal(data: string, ctx: Computation.Context) { const dataBlocks: Data.Block[] = []; - const tokenizer = createTokenizer(data); + const tokenizer = createTokenizer(data, ctx); let blockHeader: string = ''; let blockCategories = Object.create(null); @@ -521,6 +563,8 @@ function parseInternal(data: string): Result<Data.File> { //inSaveFrame = false, //blockSaveFrames: any; + ctx.updateProgress('Parsing...'); + moveNext(tokenizer); while (tokenizer.currentTokenType !== CifTokenType.End) { let token = tokenizer.currentTokenType; @@ -561,7 +605,7 @@ function parseInternal(data: string): Result<Data.File> { moveNext(tokenizer); // Loop } */ else if (token === CifTokenType.Loop) { - const cat = handleLoop(tokenizer, /*inSaveFrame ? saveFrame : */ blockCategories); + const cat = await handleLoop(tokenizer, /*inSaveFrame ? saveFrame : */ blockCategories); if (cat.hasError) { return error(cat.errorLine, cat.errorMessage); } @@ -590,5 +634,7 @@ function parseInternal(data: string): Result<Data.File> { } export default function parse(data: string) { - return parseInternal(data); + return new Computation<Result<Data.File>>(async ctx => { + return await parseInternal(data, ctx); + }); } \ No newline at end of file diff --git a/src/script.ts b/src/script.ts index e792fdc922f494cd137e667017be6cdd9b7ca541..9db88b748367dae89859011acbc3cbd713c2f066 100644 --- a/src/script.ts +++ b/src/script.ts @@ -72,9 +72,13 @@ export function _gro() { }); } -function runCIF(input: string | Uint8Array) { +async function runCIF(input: string | Uint8Array) { console.time('parseCIF'); - const parsed = typeof input === 'string' ? CIF.parseText(input) : CIF.parseBinary(input); + const comp = typeof input === 'string' ? CIF.parseText(input) : CIF.parseBinary(input); + + const running = comp.runWithContext(new Computation.ObservableContext({ updateRateMs: 250 })); + running.subscribe(p => console.log(`${(p.current / p.max * 100).toFixed(2)} (${p.elapsedMs | 0}ms)`)); + const parsed = await running.result; console.timeEnd('parseCIF'); if (parsed.isError) { console.log(parsed); @@ -94,7 +98,7 @@ function runCIF(input: string | Uint8Array) { export function _cif() { let path = `./examples/1cbs_updated.cif`; - //path = 'c:/test/quick/3j3q.cif'; + path = 'c:/test/quick/3j3q.cif'; fs.readFile(path, 'utf8', function (err, input) { if (err) { return console.log(err); @@ -122,7 +126,7 @@ _cif(); import Computation from './utils/computation' const comp = new Computation(async ctx => { - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 0; i++) { await new Promise(res => setTimeout(res, 500)); if (ctx.requiresUpdate) await ctx.updateProgress('working', void 0, i, 2); } diff --git a/src/utils/computation.ts b/src/utils/computation.ts index 0206208cc4d48eed1100d6f7a5b655d5479cfd12..7730dd7960880db1e8e9d05d8723b2e7aa68cd63 100644 --- a/src/utils/computation.ts +++ b/src/utils/computation.ts @@ -56,11 +56,12 @@ namespace Computation { export const Aborted = 'Aborted'; export interface Progress { - message: string; - isIndeterminate: boolean; - current: number; - max: number; - requestAbort?: () => void; + message: string, + isIndeterminate: boolean, + current: number, + max: number, + elapsedMs: number, + requestAbort?: () => void } export interface Context { @@ -69,6 +70,8 @@ namespace Computation { /** * Checks if the computation was aborted. If so, throws. * Otherwise, updates the progress. + * + * Returns the number of ms since the last update. */ updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number): Promise<void> | void } @@ -87,13 +90,16 @@ namespace Computation { } export class ObservableContext implements Context { - private updateRate: number; + readonly updateRate: number; private isSynchronous: boolean; private level = 0; + private startedTime = 0; private abortRequested = false; private lastUpdated = 0; private observers: ProgressObserver[] | undefined = void 0; - private progress: Progress = { message: 'Working...', current: 0, max: 0, isIndeterminate: true, requestAbort: void 0 }; + private progress: Progress = { message: 'Working...', current: 0, max: 0, elapsedMs: 0, isIndeterminate: true, requestAbort: void 0 }; + + lastDelta = 0; private checkAborted() { if (this.abortRequested) throw Aborted; @@ -117,6 +123,8 @@ namespace Computation { updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number): Promise<void> | void { this.checkAborted(); + const time = Helpers.getTime(); + if (typeof abort === 'boolean') { this.progress.requestAbort = abort ? this.abortRequester : void 0; } else { @@ -125,6 +133,7 @@ namespace Computation { } this.progress.message = msg; + this.progress.elapsedMs = time - this.startedTime; if (isNaN(current!)) { this.progress.isIndeterminate = true; } else { @@ -138,7 +147,8 @@ namespace Computation { for (const o of this.observers) setTimeout(o, 0, p); } - this.lastUpdated = Helpers.getTime(); + this.lastDelta = time - this.lastUpdated; + this.lastUpdated = time; return new Promise<void>(res => setTimeout(res, 0)); } @@ -146,10 +156,11 @@ namespace Computation { get requiresUpdate() { this.checkAborted(); if (this.isSynchronous) return false; - return Helpers.getTime() - this.lastUpdated > this.updateRate; + return Helpers.getTime() - this.lastUpdated > this.updateRate / 2; } started() { + if (!this.level) this.startedTime = Helpers.getTime(); this.level++; } @@ -161,11 +172,46 @@ namespace Computation { if (!this.level) this.observers = void 0; } - constructor(params?: Params) { + constructor(params?: Partial<Params>) { this.updateRate = (params && params.updateRateMs) || DefaulUpdateRateMs; this.isSynchronous = !!(params && params.isSynchronous); } } + + export class Chunked { + private currentChunkSize: number; + + private computeChunkSize() { + const lastDelta = (this.context as ObservableContext).lastDelta || 0; + if (!lastDelta) return this.defaultChunkSize; + const rate = (this.context as ObservableContext).updateRate || 0; + return Math.round(this.currentChunkSize * rate / lastDelta + 1); + } + + get chunkSize() { + return this.defaultChunkSize; + } + + set chunkSize(value: number) { + this.defaultChunkSize = value; + this.currentChunkSize = value; + } + + get requiresUpdate() { + const ret = this.context.requiresUpdate; + if (!ret) this.currentChunkSize += this.chunkSize; + return ret; + } + + async updateProgress(msg: string, abort?: boolean | (() => void), current?: number, max?: number) { + await this.context.updateProgress(msg, abort, current, max); + this.defaultChunkSize = this.computeChunkSize(); + } + + constructor(public context: Context, private defaultChunkSize: number) { + this.currentChunkSize = defaultChunkSize; + } + } } namespace Helpers {