lazy loading, cached TUR values

This commit is contained in:
sawka 2022-08-12 18:34:56 -07:00
parent b4e41bc36d
commit 7af1d5cee6
3 changed files with 196 additions and 94 deletions

View File

@ -14,6 +14,38 @@ import {GlobalModel, GlobalInput, Session, Cmd, Window, Screen, ScreenWindow} fr
dayjs.extend(localizedFormat)
type InterObsValue = {
sessionid : string,
windowid : string,
lineid : string,
cmdid : string,
visible : mobx.IObservableValue<boolean>,
timeoutid? : any,
};
let globalLineWeakMap = new WeakMap<any, InterObsValue>();
function interObsCallback(entries) {
let now = Date.now();
entries.forEach((entry) => {
let line = globalLineWeakMap.get(entry.target);
if ((line.timeoutid != null) && (line.visible.get() == entry.isIntersecting)) {
clearTimeout(line.timeoutid);
line.timeoutid = null;
return;
}
if (line.visible.get() != entry.isIntersecting && line.timeoutid == null) {
line.timeoutid = setTimeout(() => {
line.timeoutid = null;
mobx.action(() => {
line.visible.set(entry.isIntersecting);
})();
}, 250);
return;
}
});
}
function getLineId(line : LineType) : string {
return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid);
}
@ -107,30 +139,78 @@ class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
}
@mobxReact.observer
class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width: number}, {}> {
class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width : number, interObs : IntersectionObserver, initVis : boolean}, {}> {
termLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false);
lineRef : React.RefObject<any> = React.createRef();
iobsVal : InterObsValue = null;
autorunDisposer : () => void = null;
constructor(props) {
super(props);
let line = props.line;
let ival : InterObsValue = {
sessionid: line.sessionid,
windowid: line.windowid,
lineid: line.lineid,
cmdid: line.cmdid,
visible: mobx.observable.box(this.props.initVis),
};
this.iobsVal = ival;
}
componentDidMount() {
visibilityChanged(vis : boolean) : void {
if (vis && !this.termLoaded.get()) {
this.loadTerminal();
}
else if (!vis && this.termLoaded.get()) {
let {line} = this.props;
}
}
loadTerminal() : void {
let {sw, line} = this.props;
let model = GlobalModel;
let cmd = model.getCmd(line);
if (cmd != null) {
let termElem = document.getElementById("term-" + getLineId(line));
cmd.connectElem(termElem, sw.screenId, sw.windowId, this.props.width);
mobx.action(() => this.termLoaded.set(true))();
if (cmd == null) {
return;
}
let termId = "term-" + getLineId(line);
let termElem = document.getElementById(termId);
if (termElem == null) {
console.log("cannot load terminal, no term elem found", termId);
return;
}
sw.connectElem(termElem, cmd, this.props.width);
mobx.action(() => this.termLoaded.set(true))();
}
componentDidMount() {
let {line} = this.props;
if (this.lineRef.current == null || this.props.interObs == null) {
console.log("LineCmd lineRef current is null or interObs is null", line, this.lineRef.current, this.props.interObs);
}
else {
globalLineWeakMap.set(this.lineRef.current, this.iobsVal);
this.props.interObs.observe(this.lineRef.current);
this.autorunDisposer = mobx.autorun(() => {
let vis = this.iobsVal.visible.get();
this.visibilityChanged(vis);
});
}
}
componentWillUnmount() {
let {sw, line} = this.props;
let model = GlobalModel;
let cmd = model.getCmd(line);
if (cmd != null) {
cmd.disconnectElem(sw.screenId, sw.windowId);
if (this.termLoaded.get()) {
sw.disconnectElem(line.cmdid);
}
if (this.lineRef.current != null && this.props.interObs != null) {
this.props.interObs.unobserve(this.lineRef.current);
}
if (this.autorunDisposer != null) {
this.autorunDisposer();
}
}
@ -143,12 +223,9 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
doRefresh() {
let {sw, line} = this.props;
let model = GlobalModel;
let cmd = model.getCmd(line);
if (cmd != null) {
let termWrap = cmd.getTermWrap(sw.screenId, sw.windowId);
if (termWrap != null) {
termWrap.reloadTerminal(500);
}
let termWrap = sw.getTermWrap(line.cmdid);
if (termWrap != null) {
termWrap.reloadTerminal(500);
}
}
@ -170,28 +247,32 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
}
render() {
let {sw, line} = this.props;
let {sw, line, width} = this.props;
let model = GlobalModel;
let lineid = line.lineid.toString();
let formattedTime = getLineDateStr(line.ts);
let cmd = model.getCmd(line);
if (cmd == null) {
return <div className="line line-invalid">[cmd not found '{line.cmdid}']</div>;
return (
<div className="line line-invalid" id={"line-" + getLineId(line)} ref={this.lineRef}>
[cmd not found '{line.cmdid}']
</div>
);
}
let termLoaded = this.termLoaded.get();
let cellHeightPx = 16;
let cellWidthPx = 8;
let termWidth = Math.max(Math.trunc((this.props.width - 20)/cellWidthPx), 10);
let usedRows = cmd.getUsedRows(sw.screenId, sw.windowId);
let termWidth = Math.max(Math.trunc((width - 20)/cellWidthPx), 10);
let usedRows = sw.getUsedRows(cmd, width);
let totalHeight = cellHeightPx * usedRows;
let remote = model.getRemote(cmd.remoteId);
let status = cmd.getStatus();
let running = (status == "running");
let detached = (status == "detached");
let termOpts = cmd.getTermOpts();
let isFocused = cmd.getIsFocused(sw.screenId, sw.windowId);
let isFocused = sw.getIsFocused(line.cmdid);
return (
<div className={cn("line", "line-cmd", {"focus": isFocused})} id={"line-" + getLineId(line)}>
<div className={cn("line", "line-cmd", {"focus": isFocused})} id={"line-" + getLineId(line)} ref={this.lineRef} style={{position: "relative"}}>
<div className="line-header">
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})} onClick={this.doRefresh}>
{lineid}
@ -212,6 +293,7 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
</div>
<div className={cn("terminal-wrapper", {"focus": isFocused})} style={{overflowY: "hidden"}}>
<div className="terminal" id={"term-" + getLineId(line)} data-cmdid={line.cmdid} style={{height: totalHeight}}></div>
<If condition={!termLoaded}><div style={{position: "absolute", top: 60, left: 30}}>(loading)</div></If>
</div>
</div>
);
@ -219,7 +301,7 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
}
@mobxReact.observer
class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number}, {}> {
class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number, interObs : IntersectionObserver, initVis : boolean}, {}> {
render() {
let line = this.props.line;
if (line.linetype == "text") {
@ -237,7 +319,6 @@ class CmdInput extends React.Component<{}, {}> {
lastTab : boolean = false;
lastHistoryUpDown : boolean = false;
lastTabCurLine : mobx.IObservableValue<string> = mobx.observable.box(null);
textareaRef : React.RefObject<any> = React.createRef();
isModKeyPress(e : any) {
return e.code.match(/^(Control|Meta|Alt|Shift)(Left|Right)$/);
@ -423,7 +504,7 @@ class CmdInput extends React.Component<{}, {}> {
<div className="button is-static">{promptStr}</div>
</div>
<div className="control cmd-input-control is-expanded">
<textarea id="main-cmd-input" ref={this.textareaRef} rows={displayLines} value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className="textarea"></textarea>
<textarea id="main-cmd-input" rows={displayLines} value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className="textarea"></textarea>
</div>
<div className="control cmd-exec">
<div onClick={this.doSubmitCmd} className="button">
@ -441,7 +522,8 @@ class CmdInput extends React.Component<{}, {}> {
@mobxReact.observer
class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
mutObs : any;
rszObs : any
rszObs : any;
interObs : IntersectionObserver;
randomId : string;
width : mobx.IObservableValue<number> = mobx.observable.box(0);
lastHeight : number = null;
@ -462,7 +544,7 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
let target = event.target;
let atBottom = (target.scrollTop + 30 > (target.scrollHeight - target.offsetHeight));
if (sw && sw.shouldFollow.get() != atBottom) {
mobx.action(() => sw.shouldFollow.set(atBottom));
mobx.action(() => sw.shouldFollow.set(atBottom))();
}
// console.log("scroll-handler>", atBottom, target.scrollTop, target.scrollHeight);
}
@ -477,6 +559,11 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
if (sw && sw.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom("mount"), 0);
}
this.interObs = new IntersectionObserver(interObsCallback, {
root: elem,
rootMargin: "800px",
threshold: 0.0,
});
}
let wvElem = document.getElementById(this.getWindowViewDOMId());
if (wvElem != null) {
@ -498,6 +585,9 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
if (this.rszObs) {
this.rszObs.disconnect();
}
if (this.interObs) {
this.interObs.disconnect();
}
}
handleResize(entries : any) {
@ -619,7 +709,7 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
</div>
<div key="lines" className="lines" onScroll={this.scrollHandler} id={this.getLinesDOMId()} style={linesStyle}>
<For each="line" of={win.lines} index="idx">
<Line key={line.lineid} line={line} sw={sw} width={this.width.get()}/>
<Line key={line.lineid} line={line} sw={sw} width={this.width.get()} interObs={this.interObs} initVis={idx > win.lines.length-1-7}/>
</For>
</div>
<If condition={win.lines.length == 0}>

View File

@ -51,7 +51,6 @@ class Cmd {
cmdId : string;
data : OV<CmdDataType>;
watching : boolean = false;
instances : Record<string, TermWrap> = {};
constructor(cmd : CmdDataType) {
this.sessionId = cmd.sessionid;
@ -60,58 +59,6 @@ class Cmd {
this.data = mobx.observable.box(cmd, {deep: false});
}
connectElem(elem : Element, screenId : string, windowId : string, width : number) {
let termWrap = this.getTermWrap(screenId, windowId);
if (termWrap != null) {
console.log("term-wrap already exists for", screenId, windowId);
return;
}
termWrap = new TermWrap(elem, this.sessionId, this.cmdId, 0, this.getTermOpts(), {height: 0, width: width}, this.handleKey.bind(this));
this.instances[screenId + "/" + windowId] = termWrap;
return;
}
disconnectElem(screenId : string, windowId : string) {
let key = screenId + "/" + windowId;
let termWrap = this.instances[key];
if (termWrap != null) {
termWrap.dispose();
delete this.instances[key];
}
}
updatePtyData(ptyMsg : PtyDataUpdateType) {
for (let key in this.instances) {
let tw = this.instances[key];
let data = base64ToArray(ptyMsg.ptydata64);
tw.updatePtyData(ptyMsg.ptypos, data);
}
}
getTermWrap(screenId : string, windowId : string) : TermWrap {
return this.instances[screenId + "/" + windowId];
}
getUsedRows(screenId : string, windowId : string) : number {
let termOpts = this.getTermOpts();
if (!termOpts.flexrows) {
return termOpts.rows;
}
let termWrap = this.getTermWrap(screenId, windowId);
if (termWrap == null) {
return 2;
}
return termWrap.usedRows.get();
}
getIsFocused(screenId : string, windowId : string) : boolean {
let termWrap = this.getTermWrap(screenId, windowId);
if (termWrap == null) {
return false;
}
return termWrap.isFocused.get();
}
setCmd(cmd : CmdDataType) {
mobx.action(() => {
this.data.set(cmd);
@ -205,10 +152,7 @@ class Screen {
updatePtyData(ptyMsg : PtyDataUpdateType) {
for (let i=0; i<this.windows.length; i++) {
let sw = this.windows[i];
let win = sw.getWindow();
if (win != null) {
win.updatePtyData(ptyMsg);
}
sw.updatePtyData(ptyMsg);
}
}
@ -237,6 +181,9 @@ class ScreenWindow {
layout : OV<LayoutType>;
shouldFollow : OV<boolean> = mobx.observable.box(true);
// cmdid => TermWrap
terms : Record<string, TermWrap> = {};
constructor(swdata : ScreenWindowType) {
this.sessionId = swdata.sessionid;
this.screenId = swdata.screenid;
@ -245,6 +192,65 @@ class ScreenWindow {
this.layout = mobx.observable.box(swdata.layout);
}
updatePtyData(ptyMsg : PtyDataUpdateType) {
let cmdId = ptyMsg.cmdid;
let term = this.terms[cmdId];
if (term == null) {
return;
}
let data = base64ToArray(ptyMsg.ptydata64);
term.updatePtyData(ptyMsg.ptypos, data);
}
getTermWrap(cmdId : string) : TermWrap {
return this.terms[cmdId];
}
connectElem(elem : Element, cmd : Cmd, width : number) {
let cmdId = cmd.cmdId;
let termWrap = this.getTermWrap(cmdId);
if (termWrap != null) {
console.log("term-wrap already exists for", this.screenId, this.windowId, cmdId);
return;
}
let usedRows = GlobalModel.getTUR(this.sessionId, cmdId, width);
termWrap = new TermWrap(elem, this.sessionId, cmdId, usedRows, cmd.getTermOpts(), {height: 0, width: width}, cmd.handleKey.bind(cmd));
this.terms[cmdId] = termWrap;
return;
}
disconnectElem(cmdId : string) {
let termWrap = this.terms[cmdId];
if (cmdId != null) {
termWrap.dispose();
delete this.terms[cmdId];
}
}
getUsedRows(cmd : Cmd, width : number) : number {
let termOpts = cmd.getTermOpts();
if (!termOpts.flexrows) {
return termOpts.rows;
}
let termWrap = this.getTermWrap(cmd.cmdId);
if (termWrap == null) {
let usedRows = GlobalModel.getTUR(this.sessionId, cmd.cmdId, width);
if (usedRows != null) {
return usedRows;
}
return 2;
}
return termWrap.usedRows.get();
}
getIsFocused(cmdId : string) : boolean {
let termWrap = this.getTermWrap(cmdId);
if (termWrap == null) {
return false;
}
return termWrap.isFocused.get();
}
reset() {
mobx.action(() => {
this.shouldFollow.set(true);
@ -271,14 +277,6 @@ class Window {
this.windowId = windowId;
}
updatePtyData(ptyMsg : PtyDataUpdateType) {
let cmd = this.cmds[ptyMsg.cmdid];
if (cmd == null) {
return;
}
cmd.updatePtyData(ptyMsg);
}
updateWindow(win : WindowDataType, load : boolean) {
mobx.action(() => {
if (!isBlank(win.curremote)) {
@ -652,6 +650,7 @@ class Model {
infoMsg : OV<InfoType> = mobx.observable.box(null);
infoTimeoutId : any = null;
inputModel : InputModel;
termUsedRowsCache : Record<string, number> = {};
constructor() {
this.clientId = getApi().getId();
@ -666,6 +665,16 @@ class Model {
getApi().onDigitCmd(this.onDigitCmd.bind(this));
}
getTUR(sessionId : string, cmdId : string, width : number) : number {
let key = sessionId + "/" + cmdId + "/" + width;
return this.termUsedRowsCache[key];
}
setTUR(sessionId : string, cmdId : string, width : number, usedRows : number) : void {
let key = sessionId + "/" + cmdId + "/" + width;
this.termUsedRowsCache[key] = usedRows;
}
contextScreen(e : any, screenId : string) {
console.log("model", screenId);
getApi().contextScreen({screenId: screenId}, {x: e.x, y: e.y});

View File

@ -46,15 +46,17 @@ class TermWrap {
reloading : boolean = false;
dataUpdates : DataUpdate[] = [];
loadError : mobx.IObservableValue<boolean> = mobx.observable.box(false);
winSize : WindowSize;
constructor(elem : Element, sessionId : string, cmdId : string, usedRows : number, termOpts : TermOptsType, winSize : WindowSize, keyHandler : (event : any) => void) {
this.sessionId = sessionId;
this.cmdId = cmdId;
this.connectedElem = elem;
this.flexRows = termOpts.flexrows ?? false;
this.winSize = winSize;
if (this.flexRows) {
this.atRowMax = false;
this.usedRows = mobx.observable.box(usedRows || 2);
this.usedRows = mobx.observable.box(usedRows ?? 2);
}
else {
this.atRowMax = true;
@ -141,6 +143,7 @@ class TermWrap {
mobx.action(() => {
let oldUsedRows = this.usedRows.get();
this.usedRows.set(tur);
GlobalModel.setTUR(this.sessionId, this.cmdId, this.winSize.width, tur);
if (this.connectedElem) {
let resizeEvent = new CustomEvent("termresize", {
bubbles: true,