Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new-log-viewer: Add log query support in StateContextProvider. #80

Merged
merged 29 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
49bd9a4
query log structure demo (won't work)
Henry8192 Sep 23, 2024
5b48fd3
clean up queryLog
Henry8192 Sep 27, 2024
7a777ee
various changes
Henry8192 Sep 30, 2024
fdd761a
finish draft of query log
Henry8192 Oct 1, 2024
5e4571a
queryResults should useRef
Henry8192 Oct 1, 2024
6be07e9
add test query icon to menubar
Henry8192 Oct 2, 2024
908ee7a
fix query log button, query logs now appear properly in console
Henry8192 Oct 2, 2024
da20db4
remove console logs, rename handleChunkResult to chunkResultsHandler …
Henry8192 Oct 6, 2024
5bf7df1
remove console logs in debugging
Henry8192 Oct 6, 2024
9a3cca9
address suggested changes in pr
Henry8192 Oct 7, 2024
8aa0c14
add parameters for queryLogs()
Henry8192 Oct 7, 2024
38fe8e1
address part of the suggestions in code review
Henry8192 Oct 9, 2024
fb14c88
minor fixes
Henry8192 Oct 9, 2024
47c2b82
Merge branch 'main' into log-query
Henry8192 Oct 10, 2024
1286879
address suggestions from review after merge
Henry8192 Oct 10, 2024
ea6b1ee
Merge branch 'main' into log-query
Henry8192 Oct 15, 2024
7ed8baf
use Map instead of Object for query results
Henry8192 Oct 15, 2024
46fe49f
fix queryResults not showing issue
Henry8192 Oct 15, 2024
9706bd0
provide queryResults in StateContextProvider.tsx
Henry8192 Oct 15, 2024
8e91e83
fix lint errors
Henry8192 Oct 15, 2024
8f60afc
fix lint docstring
Henry8192 Oct 15, 2024
3de7f80
Apply suggestions from code review
Henry8192 Oct 16, 2024
3c6c53d
Apply suggestions from code review
Henry8192 Oct 16, 2024
e04f169
address rest of the comments
Henry8192 Oct 16, 2024
e3913d5
fix lint comment warning
Henry8192 Oct 16, 2024
b732607
Update new-log-viewer/src/utils/config.ts
Henry8192 Oct 17, 2024
45e5e61
Apply suggestions from code review
Henry8192 Oct 19, 2024
142275e
apply suggestions
Henry8192 Oct 19, 2024
24e86c8
Apply suggestions from code review
Henry8192 Oct 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion new-log-viewer/src/components/MenuBar/index.tsx
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import DescriptionIcon from "@mui/icons-material/Description";
import FileOpenIcon from "@mui/icons-material/FileOpen";
import SearchIcon from "@mui/icons-material/Search";
import SettingsIcon from "@mui/icons-material/Settings";

import {StateContext} from "../../contexts/StateContextProvider";
Expand All @@ -31,10 +32,14 @@ import "./index.css";
* @return
*/
const MenuBar = () => {
const {fileName, loadFile} = useContext(StateContext);
const {fileName, loadFile, queryLogs} = useContext(StateContext);

const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);

const handleSearchButtonClick = () => {
queryLogs("scheduled", false, false);
};
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved

const handleOpenFileButtonClick = () => {
openFile((file) => {
loadFile(file, {code: CURSOR_CODE.LAST_EVENT, args: null});
Expand Down Expand Up @@ -78,6 +83,11 @@ const MenuBar = () => {
<SettingsIcon/>
</SmallIconButton>
<ExportLogsButton/>
<SmallIconButton
onClick={handleSearchButtonClick}
>
<SearchIcon/>
</SmallIconButton>
</Sheet>
<SettingsModal
isOpen={isSettingsModalOpen}
Expand Down
35 changes: 34 additions & 1 deletion new-log-viewer/src/contexts/StateContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {CONFIG_KEY} from "../typings/config";
import {SEARCH_PARAM_NAMES} from "../typings/url";
import {
BeginLineNumToLogEventNumMap,
ChunkResults,
CURSOR_CODE,
CursorType,
FileSrcType,
Expand Down Expand Up @@ -51,6 +52,7 @@ interface StateContextType {
exportLogs: () => void,
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
loadPage: (newPageNum: number) => void,
queryLogs: (searchString: string, isRegex: boolean, isCaseSensitive: boolean) => void,
}
const StateContext = createContext<StateContextType>({} as StateContextType);

Expand All @@ -69,6 +71,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
exportLogs: () => null,
loadFile: () => null,
loadPage: () => null,
queryLogs: () => null,
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
});

interface StateContextProviderProps {
Expand Down Expand Up @@ -137,7 +140,7 @@ const workerPostReq = <T extends WORKER_REQ_CODE>(
* @param props.children
* @return
*/
// eslint-disable-next-line max-lines-per-function
// eslint-disable-next-line max-lines-per-function,max-statements
const StateContextProvider = ({children}: StateContextProviderProps) => {
const {filePath, logEventNum} = useContext(UrlContext);

Expand All @@ -157,6 +160,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
const logExportManagerRef = useRef<null|LogExportManager>(null);
const mainWorkerRef = useRef<null|Worker>(null);

const queryResults = useRef<ChunkResults>({});
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved

Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
const handleMainWorkerResp = useCallback((ev: MessageEvent<MainWorkerRespMessage>) => {
const {code, args} = ev.data;
console.log(`[MainWorker -> Renderer] code=${code}`);
Expand Down Expand Up @@ -184,12 +189,39 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
updateLogEventNumInUrl(lastLogEventNum, logEventNumRef.current);
break;
}
case WORKER_RESP_CODE.CHUNK_RESULT:
console.log(`[MainWorker -> Renderer] CHUNK_RESULT: ${JSON.stringify(args)}`);
for (const [pageNumStr, results] of Object.entries(args)) {
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
const pageNum = parseInt(pageNumStr, 10);
if (!queryResults.current[pageNum]) {
queryResults.current[pageNum] = [];
}
queryResults.current[pageNum].push(...results);
}
break;
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
default:
console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`);
break;
}
}, []);

const queryLogs = useCallback((
searchString: string,
isRegex: boolean,
isCaseSensitive: boolean
) => {
if (null === mainWorkerRef.current) {
junhaoliao marked this conversation as resolved.
Show resolved Hide resolved
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
console.error("Unexpected null mainWorkerRef.current");

return;
}
workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.QUERY_LOG, {
searchString: searchString,
isRegex: isRegex,
isCaseSensitive: isCaseSensitive,
});
}, []);

const exportLogs = useCallback(() => {
if (null === mainWorkerRef.current) {
console.error("Unexpected null mainWorkerRef.current");
Expand Down Expand Up @@ -335,6 +367,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
exportLogs: exportLogs,
loadFile: loadFile,
loadPage: loadPage,
queryLogs: queryLogs,
}}
>
{children}
Expand Down
103 changes: 97 additions & 6 deletions new-log-viewer/src/services/LogFileManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-lines */
// TODO: either increase max-lines or refactor the code
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
import {
Decoder,
DecoderOptionsType,
Expand All @@ -6,19 +8,23 @@ import {
import {MAX_V8_STRING_LENGTH} from "../typings/js";
import {
BeginLineNumToLogEventNumMap,
ChunkResults,
CURSOR_CODE,
CursorType,
FileSrcType,
} from "../typings/worker";
import {EXPORT_LOGS_CHUNK_SIZE} from "../utils/config";
import {getUint8ArrayFrom} from "../utils/http";
import {getChunkNum} from "../utils/math";
import {defer} from "../utils/time";
import {formatSizeInBytes} from "../utils/units";
import {getBasenameFromUrlOrDefault} from "../utils/url";
import ClpIrDecoder from "./decoders/ClpIrDecoder";
import JsonlDecoder from "./decoders/JsonlDecoder";


const SEARCH_CHUNK_SIZE = 10000;

Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Loads a file from a given source.
*
Expand Down Expand Up @@ -49,13 +55,17 @@ const loadFile = async (fileSrc: FileSrcType)
* Class to manage the retrieval and decoding of a given log file.
*/
class LogFileManager {
readonly #fileName: string;

readonly #numEvents: number = 0;

readonly #pageSize: number;

readonly #fileName: string;
#queryId: number = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we leave some docs somewhere (maybe some at the submitQuery method) to explain why we need this field? e.g.

Due to JavaScript's single-threaded nature, there is no way to receive new messages or to intercept the handling of any onmessage calls until the callback returns.
...
To allow the service worker to respond to users' cancellations or new queries in a timely manner, here we assign each query an ID before it starts.
...
The ID is incremented every time a new query is received from the user.
...
Before a search task perform its work, it should check whether its query ID matches the latest ID in the LogFileManager... If there has been a new query, the search task shall abort immediately without performing any searches and scheduling for the next chunk.


#decoder: Decoder;
readonly #chunkResultsHandler: (chunkResults: ChunkResults) => void;

#numEvents: number = 0;
#decoder: Decoder;

/**
* Private constructor for LogFileManager. This is not intended to be invoked publicly.
Expand All @@ -64,15 +74,18 @@ class LogFileManager {
* @param decoder
* @param fileName
* @param pageSize Page size for setting up pagination.
* @param chunkResultsHandler
*/
constructor (
decoder: Decoder,
fileName: string,
pageSize: number,
chunkResultsHandler: (chunkResults: ChunkResults) => void,
) {
this.#decoder = decoder;
this.#fileName = fileName;
this.#pageSize = pageSize;
this.#decoder = decoder;
this.#chunkResultsHandler = chunkResultsHandler;

// Build index for the entire file
const buildIdxResult = decoder.buildIdx(0, LOG_EVENT_FILE_END_IDX);
Expand All @@ -99,17 +112,19 @@ class LogFileManager {
* File object.
* @param pageSize Page size for setting up pagination.
* @param decoderOptions Initial decoder options.
* @param chunkResultsHandler
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
* @return A Promise that resolves to the created LogFileManager instance.
*/
static async create (
fileSrc: FileSrcType,
pageSize: number,
decoderOptions: DecoderOptionsType
decoderOptions: DecoderOptionsType,
chunkResultsHandler: (chunkResults: ChunkResults) => void,
): Promise<LogFileManager> {
const {fileName, fileData} = await loadFile(fileSrc);
const decoder = await LogFileManager.#initDecoder(fileName, fileData, decoderOptions);

return new LogFileManager(decoder, fileName, pageSize);
return new LogFileManager(decoder, fileName, pageSize, chunkResultsHandler);
}

/**
Expand Down Expand Up @@ -153,6 +168,10 @@ class LogFileManager {
this.#decoder.setDecoderOptions(options);
}

incrementQueryId () {
this.#queryId++;
}
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Loads log events in the range
* [`beginLogEventIdx`, `beginLogEventIdx + EXPORT_LOGS_CHUNK_SIZE`), or all remaining log
Expand Down Expand Up @@ -230,6 +249,78 @@ class LogFileManager {
};
}

/**
* Searches for log events based on the given search string.
*
* @param searchString The search string.
* @param isRegex Whether the search string is a regular expression.
* @param matchCase Whether the search is case-sensitive.
* @return An object containing the search results.
*/
startQuery (searchString: string, isRegex: boolean, matchCase: boolean): void {
this.incrementQueryId();

// If the search string is empty, or there are no logs, return
if ("" === searchString) {
return;
} else if (0 === this.#numEvents) {
return;
}

// Construct search RegExp
const regexPattern = isRegex ?
searchString :
searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regexFlags = matchCase ?
"" :
"i";
const searchRegex = new RegExp(regexPattern, regexFlags);
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
this.#searchChunk(this.#queryId, 0, searchRegex);
}

/**
* Searches for log events in the given range.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param queryId
* @param beginSearchIdx The beginning index of the search range.
* @param searchRegex The regular expression to search
* @return
*/
#searchChunk (queryId: number, beginSearchIdx: number, searchRegex: RegExp): void {
if (queryId !== this.#queryId) {
return;
}

const endSearchIdx = Math.min(beginSearchIdx + SEARCH_CHUNK_SIZE, this.#numEvents);
const results: ChunkResults = {};

for (let eventIdx = beginSearchIdx; eventIdx < endSearchIdx; eventIdx++) {
const contentString = this.#decoder.decode(eventIdx, eventIdx + 1)?.[0]?.[0] || "";
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
const match = contentString.match(searchRegex);
if (match && "number" === typeof match.index) {
const logEventNum = eventIdx + 1;
const pageNum = Math.ceil(logEventNum / this.#pageSize);
if (!results[pageNum]) {
results[pageNum] = [];
}
results[pageNum].push({
logEventNum: logEventNum,
message: contentString,
matchRange: [match.index,
(match.index + match[0].length)],
});
}
}
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved

if (endSearchIdx < this.#numEvents) {
defer(() => {
this.#searchChunk(queryId, endSearchIdx, searchRegex);
});
}

this.#chunkResultsHandler(results);
}

/**
* Gets the range of log event numbers for the page containing the given cursor.
*
Expand Down
31 changes: 30 additions & 1 deletion new-log-viewer/src/services/MainWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dayjsUtc from "dayjs/plugin/utc";

import {LOG_LEVEL} from "../typings/logs";
import {
ChunkResults,
MainWorkerReqMessage,
WORKER_REQ_CODE,
WORKER_RESP_CODE,
Expand Down Expand Up @@ -36,6 +37,16 @@ const postResp = <T extends WORKER_RESP_CODE>(
postMessage({code, args});
};


/**
* Post a response of a query chunk.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param chunkResults
*/
const chunkResultsHandler = (chunkResults: ChunkResults) => {
postResp(WORKER_RESP_CODE.CHUNK_RESULT, chunkResults);
};

// eslint-disable-next-line no-warning-comments
// TODO: Break this function up into smaller functions.
// eslint-disable-next-line max-lines-per-function,max-statements
Expand Down Expand Up @@ -67,7 +78,8 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
LOG_FILE_MANAGER = await LogFileManager.create(
args.fileSrc,
args.pageSize,
args.decoderOptions
args.decoderOptions,
chunkResultsHandler
);

postResp(WORKER_RESP_CODE.LOG_FILE_INFO, {
Expand All @@ -92,6 +104,23 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
LOG_FILE_MANAGER.loadPage(args.cursor)
);
break;
case WORKER_REQ_CODE.QUERY_LOG:
if (null === LOG_FILE_MANAGER) {
throw new Error("Log file manager hasn't been initialized");
}
if (
"string" !== typeof args.searchString ||
"boolean" !== typeof args.isRegex ||
"boolean" !== typeof args.isCaseSensitive
) {
throw new Error("Invalid arguments for QUERY_LOG");
}
LOG_FILE_MANAGER.startQuery(
args.searchString,
args.isRegex,
args.isCaseSensitive
);
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
break;
default:
console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`);
break;
Expand Down
Loading