Bug 1754407 - [devtools] Use a distinct reducer for source text content and make source objects immutables. r=perftest-reviewers,devtools-reviewers,bomsy,AlexandruIonescu

This helps working on debugger performance as source objects are now immutable
and so won't trigger selector updates.

This also stop updating the object/map that contains all the text contents.

Differential Revision: https://phabricator.services.mozilla.com/D138261
This commit is contained in:
Alexandre Poirot 2022-03-11 10:23:33 +00:00
Родитель 3976d30b56
Коммит a736d67240
42 изменённых файлов: 339 добавлений и 355 удалений

Просмотреть файл

@ -4,7 +4,8 @@
import {
hasInScopeLines,
getSourceWithContent,
getSource,
getSourceTextContent,
getVisibleSelectedFrame,
} from "../../selectors";
@ -28,7 +29,8 @@ function getOutOfScopeLines(outOfScopeLocations) {
}
async function getInScopeLines(cx, location, { dispatch, getState, parser }) {
const source = getSourceWithContent(getState(), location.sourceId);
const source = getSource(getState(), location.sourceId);
const sourceTextContent = getSourceTextContent(getState(), source.id);
let locations = null;
if (location.line && source && !source.isWasm) {
@ -37,9 +39,9 @@ async function getInScopeLines(cx, location, { dispatch, getState, parser }) {
const linesOutOfScope = getOutOfScopeLines(locations);
const sourceNumLines =
!source.content || !isFulfilled(source.content)
!sourceTextContent || !isFulfilled(sourceTextContent)
? 0
: getSourceLineCount(source.content.value);
: getSourceLineCount(sourceTextContent.value);
const noLinesOutOfScope =
linesOutOfScope == null || linesOutOfScope.size == 0;
@ -72,9 +74,12 @@ export function setInScopeLines(cx) {
}
const { location } = visibleFrame;
const { content } = getSourceWithContent(getState(), location.sourceId);
const sourceTextContent = getSourceTextContent(
getState(),
location.sourceId
);
if (hasInScopeLines(getState(), location) || !content) {
if (hasInScopeLines(getState(), location) || !sourceTextContent) {
return;
}

Просмотреть файл

@ -14,7 +14,8 @@ import { renderWasmText } from "../utils/wasm";
import { getMatches } from "../workers/search";
import {
getSelectedSourceWithContent,
getSelectedSourceId,
getSelectedSourceTextContent,
getFileSearchModifiers,
getFileSearchQuery,
getFileSearchResults,
@ -29,8 +30,8 @@ import { isFulfilled } from "../utils/async-value";
export function doSearch(cx, query, editor) {
return ({ getState, dispatch }) => {
const selectedSource = getSelectedSourceWithContent(getState());
if (!selectedSource || !selectedSource.content) {
const sourceTextContent = getSelectedSourceTextContent(getState());
if (!sourceTextContent) {
return;
}
@ -41,10 +42,11 @@ export function doSearch(cx, query, editor) {
export function doSearchForHighlight(query, editor, line, ch) {
return async ({ getState, dispatch }) => {
const selectedSource = getSelectedSourceWithContent(getState());
if (!selectedSource?.content) {
const sourceTextContent = getSelectedSourceTextContent(getState());
if (!sourceTextContent) {
return;
}
dispatch(searchContentsForHighlight(query, editor, line, ch));
};
}
@ -81,18 +83,17 @@ export function updateSearchResults(cx, characterIndex, line, matches) {
export function searchContents(cx, query, editor, focusFirstResult = true) {
return async ({ getState, dispatch }) => {
const modifiers = getFileSearchModifiers(getState());
const selectedSource = getSelectedSourceWithContent(getState());
const sourceTextContent = getSelectedSourceTextContent(getState());
if (
!editor ||
!selectedSource ||
!selectedSource.content ||
!isFulfilled(selectedSource.content) ||
!sourceTextContent ||
!isFulfilled(sourceTextContent) ||
!modifiers
) {
return;
}
const selectedContent = selectedSource.content.value;
const selectedContent = sourceTextContent.value;
const ctx = { ed: editor, cm: editor.codeMirror };
@ -103,7 +104,8 @@ export function searchContents(cx, query, editor, focusFirstResult = true) {
let text;
if (selectedContent.type === "wasm") {
text = renderWasmText(selectedSource.id, selectedContent).join("\n");
const selectedSourceId = getSelectedSourceId(getState());
text = renderWasmText(selectedSourceId, selectedContent).join("\n");
} else {
text = selectedContent.value;
}
@ -124,15 +126,9 @@ export function searchContents(cx, query, editor, focusFirstResult = true) {
export function searchContentsForHighlight(query, editor, line, ch) {
return async ({ getState, dispatch }) => {
const modifiers = getFileSearchModifiers(getState());
const selectedSource = getSelectedSourceWithContent(getState());
const sourceTextContent = getSelectedSourceTextContent(getState());
if (
!query ||
!editor ||
!selectedSource ||
!selectedSource.content ||
!modifiers
) {
if (!query || !editor || !sourceTextContent || !modifiers) {
return;
}

Просмотреть файл

@ -6,7 +6,7 @@ import { PROMISE } from "../utils/middleware/promise";
import {
getSource,
getSourceFromId,
getSourceWithContent,
getSourceTextContent,
getSourceContent,
getGeneratedSource,
getSourcesEpoch,
@ -137,9 +137,9 @@ export const loadSourceText = memoizeableAction("loadSourceText", {
return null;
}
const { content } = getSourceWithContent(getState(), source.id);
if (!content || content.state === "pending") {
return content;
const sourceTextContent = getSourceTextContent(getState(), source.id);
if (!sourceTextContent || sourceTextContent.state === "pending") {
return sourceTextContent;
}
// This currently swallows source-load-failure since we return fulfilled

Просмотреть файл

@ -34,7 +34,7 @@ import {
getPendingSelectedLocation,
getPendingBreakpointsForSource,
getContext,
isSourceLoadingOrLoaded,
getSourceTextContent,
} from "../../selectors";
import { prefs } from "../../utils/prefs";
@ -290,7 +290,7 @@ export function newGeneratedSources(sourceResources) {
// when the HTML file has started loading
if (
isInlineScript(newSourceActor) &&
isSourceLoadingOrLoaded(getState(), newSourceActor.source)
getSourceTextContent(getState(), newSourceActor.source) != null
) {
dispatch(setBreakableLines(cx, newSourceActor.source)).catch(error => {
if (!(error instanceof ContextError)) {

Просмотреть файл

@ -33,7 +33,7 @@ import {
canPrettyPrintSource,
getIsCurrentThreadPaused,
getSourceFromId,
getSourceWithContent,
getSourceTextContent,
tabExists,
} from "../../selectors";
@ -145,14 +145,14 @@ export function selectLocation(cx, location, { keepContext = true } = {}) {
return;
}
const sourceWithContent = getSourceWithContent(getState(), source.id);
const sourceTextContent = getSourceTextContent(getState(), source.id);
if (
keepContext &&
prefs.autoPrettyPrint &&
!getPrettySource(getState(), loadedSource.id) &&
canPrettyPrintSource(getState(), loadedSource.id) &&
isMinified(sourceWithContent)
isMinified(source, sourceTextContent)
) {
await dispatch(togglePrettyPrint(cx, loadedSource.id));
dispatch(closeTab(cx, loadedSource));

Просмотреть файл

@ -17,39 +17,43 @@ import { connect } from "../../utils/connect";
import {
getVisibleSelectedFrame,
getPauseReason,
getSourceWithContent,
getSourceTextContent,
getCurrentThread,
getPausePreviewLocation,
} from "../../selectors";
function isDocumentReady(source, location) {
return location && source && source.content && hasDocument(location.sourceId);
function isDocumentReady(location, sourceTextContent) {
return location && sourceTextContent && hasDocument(location.sourceId);
}
export class DebugLine extends PureComponent {
debugExpression;
componentDidMount() {
const { why, location, source } = this.props;
this.setDebugLine(why, location, source);
const { why, location, sourceTextContent } = this.props;
this.setDebugLine(why, location, sourceTextContent);
}
componentWillUnmount() {
const { why, location, source } = this.props;
this.clearDebugLine(why, location, source);
const { why, location, sourceTextContent } = this.props;
this.clearDebugLine(why, location, sourceTextContent);
}
componentDidUpdate(prevProps) {
const { why, location, source } = this.props;
const { why, location, sourceTextContent } = this.props;
startOperation();
this.clearDebugLine(prevProps.why, prevProps.location, prevProps.source);
this.setDebugLine(why, location, source);
this.clearDebugLine(
prevProps.why,
prevProps.location,
prevProps.sourceTextContent
);
this.setDebugLine(why, location, sourceTextContent);
endOperation();
}
setDebugLine(why, location, source) {
if (!location || !isDocumentReady(source, location)) {
setDebugLine(why, location, sourceTextContent) {
if (!location || !isDocumentReady(location, sourceTextContent)) {
return;
}
const { sourceId } = location;
@ -77,8 +81,8 @@ export class DebugLine extends PureComponent {
);
}
clearDebugLine(why, location, source) {
if (!location || !isDocumentReady(source, location)) {
clearDebugLine(why, location, sourceTextContent) {
if (!location || !isDocumentReady(location, sourceTextContent)) {
return;
}
@ -115,7 +119,8 @@ const mapStateToProps = state => {
return {
frame,
location,
source: location && getSourceWithContent(state, location.sourceId),
sourceTextContent:
location && getSourceTextContent(state, location.sourceId),
why: getPauseReason(state, getCurrentThread(state)),
};
};

Просмотреть файл

@ -7,7 +7,8 @@ import { connect } from "../../utils/connect";
import classnames from "classnames";
import actions from "../../actions";
import {
getSelectedSourceWithContent,
getSelectedSource,
getSelectedSourceTextContent,
getPrettySource,
getPaneCollapse,
getContext,
@ -59,13 +60,14 @@ class SourceFooter extends PureComponent {
selectedSource,
canPrettyPrint,
togglePrettyPrint,
sourceLoaded,
} = this.props;
if (!selectedSource) {
return;
}
if (!selectedSource.content && selectedSource.isPrettyPrinted) {
if (!sourceLoaded && selectedSource.isPrettyPrinted) {
return (
<div className="action" key="pretty-loader">
<AccessibleImage className="loader spin" />
@ -78,7 +80,6 @@ class SourceFooter extends PureComponent {
}
const tooltip = L10N.getStr("sourceTabs.prettyPrint");
const sourceLoaded = !!selectedSource.content;
const type = "prettyPrint";
return (
@ -98,8 +99,7 @@ class SourceFooter extends PureComponent {
}
blackBoxButton() {
const { cx, selectedSource, toggleBlackBox } = this.props;
const sourceLoaded = selectedSource?.content;
const { cx, selectedSource, toggleBlackBox, sourceLoaded } = this.props;
if (!selectedSource) {
return;
@ -235,11 +235,13 @@ class SourceFooter extends PureComponent {
}
const mapStateToProps = state => {
const selectedSource = getSelectedSourceWithContent(state);
const selectedSource = getSelectedSource(state);
const sourceTextContent = getSelectedSourceTextContent(state);
return {
cx: getContext(state),
selectedSource,
sourceLoaded: !!sourceTextContent,
mappedSource: getGeneratedSource(state, selectedSource),
prettySource: getPrettySource(
state,

Просмотреть файл

@ -10,7 +10,7 @@ import { connect } from "../../utils/connect";
import {
getVisibleSelectedFrame,
getSelectedLocation,
getSelectedSourceWithContent,
getSelectedSourceTextContent,
getPauseCommand,
getCurrentThread,
} from "../../selectors";
@ -26,11 +26,10 @@ function isDebugLine(selectedFrame, selectedLocation) {
);
}
function isDocumentReady(selectedSource, selectedLocation) {
function isDocumentReady(selectedLocation, selectedSourceTextContent) {
return (
selectedLocation &&
selectedSource &&
selectedSource.content &&
selectedSourceTextContent &&
hasDocument(selectedLocation.sourceId)
);
}
@ -40,8 +39,11 @@ export class HighlightLine extends Component {
previousEditorLine = null;
shouldComponentUpdate(nextProps) {
const { selectedLocation, selectedSource } = nextProps;
return this.shouldSetHighlightLine(selectedLocation, selectedSource);
const { selectedLocation, selectedSourceTextContent } = nextProps;
return this.shouldSetHighlightLine(
selectedLocation,
selectedSourceTextContent
);
}
componentDidUpdate(prevProps) {
@ -52,11 +54,11 @@ export class HighlightLine extends Component {
this.completeHighlightLine(null);
}
shouldSetHighlightLine(selectedLocation, selectedSource) {
shouldSetHighlightLine(selectedLocation, selectedSourceTextContent) {
const { sourceId, line } = selectedLocation;
const editorLine = toEditorLine(sourceId, line);
if (!isDocumentReady(selectedSource, selectedLocation)) {
if (!isDocumentReady(selectedLocation, selectedSourceTextContent)) {
return false;
}
@ -72,7 +74,7 @@ export class HighlightLine extends Component {
pauseCommand,
selectedLocation,
selectedFrame,
selectedSource,
selectedSourceTextContent,
} = this.props;
if (pauseCommand) {
this.isStepping = true;
@ -82,16 +84,22 @@ export class HighlightLine extends Component {
if (prevProps) {
this.clearHighlightLine(
prevProps.selectedLocation,
prevProps.selectedSource
prevProps.selectedSourceTextContent
);
}
this.setHighlightLine(selectedLocation, selectedFrame, selectedSource);
this.setHighlightLine(
selectedLocation,
selectedFrame,
selectedSourceTextContent
);
endOperation();
}
setHighlightLine(selectedLocation, selectedFrame, selectedSource) {
setHighlightLine(selectedLocation, selectedFrame, selectedSourceTextContent) {
const { sourceId, line } = selectedLocation;
if (!this.shouldSetHighlightLine(selectedLocation, selectedSource)) {
if (
!this.shouldSetHighlightLine(selectedLocation, selectedSourceTextContent)
) {
return;
}
@ -129,8 +137,8 @@ export class HighlightLine extends Component {
);
}
clearHighlightLine(selectedLocation, selectedSource) {
if (!isDocumentReady(selectedSource, selectedLocation)) {
clearHighlightLine(selectedLocation, selectedSourceTextContent) {
if (!isDocumentReady(selectedLocation, selectedSourceTextContent)) {
return;
}
@ -155,6 +163,6 @@ export default connect(state => {
pauseCommand: getPauseCommand(state, getCurrentThread(state)),
selectedFrame: getVisibleSelectedFrame(state),
selectedLocation,
selectedSource: getSelectedSourceWithContent(state),
selectedSourceTextContent: getSelectedSourceTextContent(state),
};
})(HighlightLine);

Просмотреть файл

@ -28,7 +28,8 @@ import {
import {
getActiveSearch,
getSelectedLocation,
getSelectedSourceWithContent,
getSelectedSource,
getSelectedSourceTextContent,
getConditionalPanelLocation,
getSymbols,
getIsCurrentThreadPaused,
@ -382,6 +383,7 @@ class Editor extends PureComponent {
const {
cx,
selectedSource,
selectedSourceTextContent,
breakpointActions,
editorActions,
isPaused,
@ -412,7 +414,7 @@ class Editor extends PureComponent {
if (target.classList.contains("CodeMirror-linenumber")) {
const lineText = getLineText(
sourceId,
selectedSource.content,
selectedSourceTextContent,
line
).trim();
@ -516,20 +518,24 @@ class Editor extends PureComponent {
}
shouldScrollToLocation(nextProps, editor) {
const { selectedLocation, selectedSource } = this.props;
const {
selectedLocation,
selectedSource,
selectedSourceTextContent,
} = this.props;
if (
!editor ||
!nextProps.selectedSource ||
!nextProps.selectedLocation ||
!nextProps.selectedLocation.line ||
!nextProps.selectedSource.content
!nextProps.selectedSourceTextContent
) {
return false;
}
const isFirstLoad =
(!selectedSource || !selectedSource.content) &&
nextProps.selectedSource.content;
(!selectedSource || !selectedSourceTextContent) &&
nextProps.selectedSourceTextContent;
const locationChanged = selectedLocation !== nextProps.selectedLocation;
const symbolsChanged = nextProps.symbols != this.props.symbols;
@ -566,7 +572,7 @@ class Editor extends PureComponent {
}
setText(props, editor) {
const { selectedSource, symbols } = props;
const { selectedSource, selectedSourceTextContent, symbols } = props;
if (!editor) {
return;
@ -577,12 +583,12 @@ class Editor extends PureComponent {
return this.clearEditor();
}
if (!selectedSource.content) {
if (!selectedSourceTextContent?.value) {
return showLoading(editor);
}
if (selectedSource.content.state === "rejected") {
let { value } = selectedSource.content;
if (selectedSourceTextContent.state === "rejected") {
let { value } = selectedSourceTextContent;
if (typeof value !== "string") {
value = "Unexpected source error";
}
@ -593,7 +599,7 @@ class Editor extends PureComponent {
return showSourceText(
editor,
selectedSource,
selectedSource.content.value,
selectedSourceTextContent.value,
symbols
);
}
@ -712,12 +718,13 @@ Editor.contextTypes = {
};
const mapStateToProps = state => {
const selectedSource = getSelectedSourceWithContent(state);
const selectedSource = getSelectedSource(state);
return {
cx: getThreadContext(state),
selectedLocation: getSelectedLocation(state),
selectedSource,
selectedSourceTextContent: getSelectedSourceTextContent(state),
searchOn: getActiveSearch(state) === "file",
conditionalPanelLocation: getConditionalPanelLocation(state),
symbols: getSymbols(state, selectedSource),

Просмотреть файл

@ -8,9 +8,7 @@ import { shallow } from "enzyme";
import DebugLine from "../DebugLine";
import * as asyncValue from "../../../utils/async-value";
import { createSourceObject } from "../../../utils/test-head";
import { setDocument, toEditorLine } from "../../../utils/editor";
import { setDocument } from "../../../utils/editor";
function createMockDocument(clear) {
const doc = {
@ -30,10 +28,7 @@ function generateDefaults(editor, overrides) {
why: { type: "breakpoint" },
},
frame: null,
source: {
...createSourceObject("foo"),
content: null,
},
sourceTextContent: null,
...overrides,
};
}
@ -52,7 +47,7 @@ function render(overrides = {}) {
const props = generateDefaults(editor, overrides);
const doc = createMockDocument(clear);
setDocument(props.source.id, doc);
setDocument("foo", doc);
const component = shallow(<DebugLine.WrappedComponent {...props} />, {
lifecycleExperimental: true,
@ -62,77 +57,6 @@ function render(overrides = {}) {
describe("DebugLine Component", () => {
describe("pausing at the first location", () => {
it("should show a new debug line", async () => {
const { component, props, doc } = render({
source: {
...createSourceObject("foo"),
content: asyncValue.fulfilled({
type: "text",
value: "",
contentType: undefined,
}),
},
});
const line = 2;
const location = createLocation(line);
component.setProps({ ...props, location });
expect(doc.removeLineClass.mock.calls).toEqual([]);
expect(doc.addLineClass.mock.calls).toEqual([
[toEditorLine("foo", line), "wrapClass", "new-debug-line"],
]);
});
describe("pausing at a new location", () => {
it("should replace the first debug line", async () => {
const { props, component, clear, doc } = render({
source: {
...createSourceObject("foo"),
content: asyncValue.fulfilled({
type: "text",
value: "",
contentType: undefined,
}),
},
});
component.instance().debugExpression = { clear: jest.fn() };
const firstLine = 2;
const secondLine = 2;
component.setProps({ ...props, location: createLocation(firstLine) });
component.setProps({
...props,
frame: createLocation(secondLine),
});
expect(doc.removeLineClass.mock.calls).toEqual([
[toEditorLine("foo", firstLine), "wrapClass", "new-debug-line"],
]);
expect(doc.addLineClass.mock.calls).toEqual([
[toEditorLine("foo", firstLine), "wrapClass", "new-debug-line"],
[toEditorLine("foo", secondLine), "wrapClass", "new-debug-line"],
]);
expect(doc.markText.mock.calls).toEqual([
[
{ ch: 2, line: toEditorLine("foo", firstLine) },
{ ch: null, line: toEditorLine("foo", firstLine) },
{ className: "debug-expression to-line-end" },
],
[
{ ch: 2, line: toEditorLine("foo", secondLine) },
{ ch: null, line: toEditorLine("foo", secondLine) },
{ className: "debug-expression to-line-end" },
],
]);
expect(clear.mock.calls).toEqual([[]]);
});
});
describe("when there is no selected frame", () => {
it("should not set the debug line", () => {
const { component, props, doc } = render({ frame: null });

Просмотреть файл

@ -14,7 +14,8 @@ import { findFunctionText } from "../../utils/function";
import actions from "../../actions";
import {
getSelectedSourceWithContent,
getSelectedSource,
getSelectedSourceTextContent,
getSymbols,
getCursorPosition,
getContext,
@ -319,17 +320,23 @@ export class Outline extends Component {
}
const mapStateToProps = state => {
const selectedSource = getSelectedSourceWithContent(state);
const selectedSource = getSelectedSource(state);
const symbols = selectedSource ? getSymbols(state, selectedSource) : null;
return {
cx: getContext(state),
symbols,
selectedSource: selectedSource,
selectedSource,
cursorPosition: getCursorPosition(state),
getFunctionText: line => {
if (selectedSource) {
return findFunctionText(line, selectedSource, symbols);
const selectedSourceTextContent = getSelectedSourceTextContent(state);
return findFunctionText(
line,
selectedSource,
selectedSourceTextContent,
symbols
);
}
return null;

Просмотреть файл

@ -204,10 +204,7 @@ describe("Frames", () => {
source2: [],
};
const sources = insertResources(createInitial(), [
{ ...source1, content: null },
{ ...source2, content: null },
]);
const sources = insertResources(createInitial(), [source1, source2]);
const processedFrames = formatCallStackFrames(
frames,

Просмотреть файл

@ -10,6 +10,7 @@
import expressions, { initialExpressionState } from "./expressions";
import sourceActors from "./source-actors";
import sources, { initialSourcesState } from "./sources";
import sourcesContent, { initialSourcesContentState } from "./sources-content";
import tabs, { initialTabState } from "./tabs";
import breakpoints, { initialBreakpointsState } from "./breakpoints";
import pendingBreakpoints from "./pending-breakpoints";
@ -34,9 +35,15 @@ import { objectInspector } from "devtools/client/shared/components/reps/index";
import { createInitial } from "../utils/resource";
/**
* Note that this is only used by jest tests.
*
* Production is using loadInitialState() in main.js
*/
export function initialState() {
return {
sources: initialSourcesState(),
sourcesContent: initialSourcesContentState(),
expressions: initialExpressionState(),
sourceActors: createInitial(),
tabs: initialTabState(),
@ -61,6 +68,7 @@ export function initialState() {
export default {
expressions,
sourceActors,
sourcesContent,
sources,
tabs,
breakpoints,

Просмотреть файл

@ -22,6 +22,7 @@ CompiledModules(
"source-actors.js",
"source-tree.js",
"sources.js",
"sources-content.js",
"tabs.js",
"threads.js",
"ui.js",

Просмотреть файл

@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
/**
* Sources content reducer.
*
* This store the textual content for each source.
*/
import { pending, fulfilled, rejected } from "../utils/async-value";
export function initialSourcesContentState() {
return {
/**
* Text content of all the sources.
* This is large data, so this is only fetched on-demand for a subset of sources.
* This state attribute is mutable in order to avoid cloning this possibly large map
* on each new source. But selectors are never based on the map. Instead they only
* query elements of the map.
*
* Map(source id => AsyncValue<String>)
*/
mutableTextContentMap: new Map(),
/**
* Incremental number that is bumped each time we navigate to a new page.
*
* This is used to better handle async race condition where we mix previous page data
* with the new page. As sources are keyed by URL we may easily conflate the two page loads data.
*/
epoch: 1,
};
}
function update(state = initialSourcesContentState(), action) {
switch (action.type) {
case "LOAD_SOURCE_TEXT":
return updateSourceTextContent(state, action);
case "NAVIGATE":
return {
...initialSourcesContentState(),
epoch: state.epoch + 1,
};
}
return state;
}
/*
* Update a source's loaded text content.
*/
function updateSourceTextContent(state, action) {
// If there was a navigation between the time the action was started and
// completed, we don't want to update the store.
if (action.epoch !== state.epoch) {
return state;
}
let content;
if (action.status === "start") {
content = pending();
} else if (action.status === "error") {
content = rejected(action.error);
} else if (typeof action.value.text === "string") {
content = fulfilled({
type: "text",
value: action.value.text,
contentType: action.value.contentType,
});
} else {
content = fulfilled({
type: "wasm",
value: action.value.text,
});
}
state.mutableTextContentMap.set(action.sourceId, content);
return {
...state,
};
}
export default update;

Просмотреть файл

@ -16,7 +16,6 @@ import {
getResource,
getResourceIds,
} from "../utils/resource";
import { pending, fulfilled, rejected } from "../utils/async-value";
import { prefs } from "../utils/prefs";
export function initialSourcesState(state) {
@ -25,7 +24,6 @@ export function initialSourcesState(state) {
* All currently available sources.
*
* See create.js: `createSourceObject` method for the description of stored objects.
* This reducers will add an extra `content` attribute which is the source text for each source.
*/
sources: createInitial(),
@ -68,14 +66,6 @@ export function initialSourcesState(state) {
breakpointPositions: {},
breakableLines: {},
/**
* Incremental number that is bumped each time we navigate to a new page.
*
* This is used to better handle async race condition where we mix previous page data
* with the new page. As sources are keyed by URL we may easily conflate the two page loads data.
*/
epoch: 1,
/**
* The actual currently selected location.
* Only set if the related source is already registered in the sources reducer.
@ -178,9 +168,6 @@ function update(state = initialSourcesState(), action) {
prefs.pendingSelectedLocation = location;
return { ...state, pendingSelectedLocation: location };
case "LOAD_SOURCE_TEXT":
return updateLoadedState(state, action);
case "BLACKBOX":
if (action.status === "done") {
const { blackboxSources } = action.value;
@ -218,11 +205,9 @@ function update(state = initialSourcesState(), action) {
},
};
}
case "NAVIGATE":
return {
...initialSourcesState(state),
epoch: state.epoch + 1,
};
return initialSourcesState(state);
}
return state;
@ -242,13 +227,7 @@ function addSources(state, sources) {
plainUrls: { ...state.plainUrls },
};
state.sources = insertResources(
state.sources,
sources.map(source => ({
...source,
content: null,
}))
);
state.sources = insertResources(state.sources, sources);
for (const source of sources) {
// 1. Update the source url map
@ -388,47 +367,6 @@ function updateRootRelativeValues(
return state;
}
/*
* Update a source's loaded text content.
*/
function updateLoadedState(state, action) {
const { sourceId } = action;
// If there was a navigation between the time the action was started and
// completed, we don't want to update the store.
if (action.epoch !== state.epoch || !hasResource(state.sources, sourceId)) {
return state;
}
let content;
if (action.status === "start") {
content = pending();
} else if (action.status === "error") {
content = rejected(action.error);
} else if (typeof action.value.text === "string") {
content = fulfilled({
type: "text",
value: action.value.text,
contentType: action.value.contentType,
});
} else {
content = fulfilled({
type: "wasm",
value: action.value.text,
});
}
return {
...state,
sources: updateResources(state.sources, [
{
id: sourceId,
content,
},
]),
};
}
/*
* Update the "isBlackBoxed" property on the source objects
*/
@ -463,7 +401,7 @@ function updateBlackboxRangesForSourceUrl(
) {
if (shouldBlackBox) {
// If newRanges is an empty array, it would mean we are blackboxing the whole
// source. To do that lets reset the contentto an empty array.
// source. To do that lets reset the content to an empty array.
if (!newRanges.length) {
currentRanges[url] = [];
} else {

Просмотреть файл

@ -2,11 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import {
getSelectedSource,
getBreakpointPositionsForLine,
} from "../selectors/sources";
import { getBreakpointsList } from "../selectors/breakpoints";
import { getSelectedSource, getBreakpointPositionsForLine } from "./sources";
import { getBreakpointsList } from "./breakpoints";
import { isGenerated } from "../utils/source";
function getColumn(column, selectedSource) {

Просмотреть файл

@ -3,8 +3,8 @@
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import { createSelector } from "reselect";
import { getSelectedSource, getSourceFromId } from "../selectors/sources";
import { getBreakpointsList } from "../selectors/breakpoints";
import { getSelectedSource, getSourceFromId } from "./sources";
import { getBreakpointsList } from "./breakpoints";
import { getFilename } from "../utils/source";
import { getSelectedLocation } from "../utils/selected-location";
import { sortSelectedBreakpoints } from "../utils/breakpoint";

Просмотреть файл

@ -7,7 +7,7 @@ import {
getSelectedSource,
getSourceInSources,
getBlackBoxRanges,
} from "../selectors/sources";
} from "./sources";
import { getCurrentThreadFrames } from "./pause";
import { annotateFrames } from "../utils/pause/frames";
import { isFrameBlackBoxed } from "../utils/source";

Просмотреть файл

@ -25,6 +25,7 @@ export * from "./project-text-search";
export * from "./quick-open";
export * from "./source-actors";
export * from "./source-tree";
export * from "./sources-content";
export * from "./sources";
export * from "./tabs";
export * from "./threads";

Просмотреть файл

@ -25,6 +25,7 @@ CompiledModules(
"quick-open.js",
"source-actors.js",
"source-tree.js",
"sources-content.js",
"sources.js",
"tabs.js",
"threads.js",

Просмотреть файл

@ -3,7 +3,7 @@
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import { getThreadPauseState } from "../reducers/pause";
import { getSelectedSourceId, getSelectedLocation } from "../selectors/sources";
import { getSelectedSourceId, getSelectedLocation } from "./sources";
import { isGeneratedId } from "devtools-source-map";

Просмотреть файл

@ -0,0 +1,31 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import { asSettled } from "../utils/async-value";
import { getSelectedSource } from "../selectors/sources";
export function getSourceTextContent(state, id) {
return state.sourcesContent.mutableTextContentMap.get(id);
}
export function getSourceContent(state, id) {
const content = getSourceTextContent(state, id);
return asSettled(content);
}
export function getSelectedSourceTextContent(state) {
const source = getSelectedSource(state);
if (!source) return null;
return getSourceTextContent(state, source.id);
}
export function isSourceLoadingOrLoaded(state, sourceId) {
const content = getSourceTextContent(state, sourceId);
return content != null;
}
export function getSourcesEpoch(state) {
return state.sourcesContent.epoch;
}

Просмотреть файл

@ -24,7 +24,7 @@ import {
import { stripQuery } from "../utils/url";
import { findPosition } from "../utils/breakpoint/breakpointPositions";
import { asSettled, isFulfilled } from "../utils/async-value";
import { isFulfilled } from "../utils/async-value";
import { originalToGeneratedId } from "devtools-source-map";
import { prefs } from "../utils/prefs";
@ -34,20 +34,12 @@ import {
getSourceActor,
getSourceActors,
getBreakableLinesForSourceActors,
} from "../selectors/source-actors";
import { getAllThreads } from "../selectors/threads";
} from "./source-actors";
import { getSourceTextContent } from "./sources-content";
import { getAllThreads } from "./threads";
// This is used by tabs selectors
export const resourceAsSourceBase = memoizeResourceShallow(
({ content, ...source }) => source
);
const resourceAsSourceWithContent = memoizeResourceShallow(
({ content, ...source }) => ({
...source,
content: asSettled(content),
})
);
export const resourceAsSourceBase = memoizeResourceShallow(source => source);
export function getSourceInSources(sources, id) {
return hasResource(sources, id)
@ -79,9 +71,7 @@ function getSourcesByURLInSources(sources, urls, url) {
if (!url || !urls[url]) {
return [];
}
return urls[url].map(id =>
getMappedResource(sources, id, resourceAsSourceBase)
);
return urls[url].map(id => getSourceInSources(sources, id));
}
function getSourcesByURL(state, url) {
@ -179,10 +169,6 @@ export function getSources(state) {
return state.sources.sources;
}
export function getSourcesEpoch(state) {
return state.sources.epoch;
}
function getUrls(state) {
return state.sources.urls;
}
@ -230,30 +216,6 @@ export const getSelectedSource = createSelector(
}
);
export const getSelectedSourceWithContent = createSelector(
getSelectedLocation,
getSources,
(selectedLocation, sources) => {
const source =
selectedLocation &&
getSourceInSources(sources, selectedLocation.sourceId);
return source
? getMappedResource(sources, source.id, resourceAsSourceWithContent)
: null;
}
);
export function getSourceWithContent(state, id) {
return getMappedResource(
state.sources.sources,
id,
resourceAsSourceWithContent
);
}
export function getSourceContent(state, id) {
const { content } = getResource(state.sources.sources, id);
return asSettled(content);
}
// This is used by tests and pause reducers
export function getSelectedSourceId(state) {
const source = getSelectedSource(state);
@ -300,7 +262,7 @@ const queryAllDisplayedSources = makeShallowQuery({
});
function getAllDisplayedSources(state) {
return queryAllDisplayedSources(state.sources.sources, {
return queryAllDisplayedSources(getSources(state), {
sourcesWithUrls: state.sources.sourcesWithUrls,
projectDirectoryRoot: state.sources.projectDirectoryRoot,
chromeAndExtensionsEnabled: state.sources.chromeAndExtensionsEnabled,
@ -324,7 +286,7 @@ const getDisplayedSourceIDs = createSelector(
);
export const getDisplayedSources = createSelector(
state => state.sources.sources,
getSources,
getDisplayedSourceIDs,
(sources, idsByThread) => {
const result = {};
@ -382,7 +344,7 @@ export function isSourceWithMap(state, id) {
}
export function canPrettyPrintSource(state, id) {
const source = getSourceWithContent(state, id);
const source = getSource(state, id);
if (
!source ||
isPretty(source) ||
@ -392,8 +354,8 @@ export function canPrettyPrintSource(state, id) {
return false;
}
const sourceContent =
source.content && isFulfilled(source.content) ? source.content.value : null;
const content = getSourceTextContent(state, id);
const sourceContent = content && isFulfilled(content) ? content.value : null;
if (!sourceContent || !isJavaScript(source, sourceContent)) {
return false;
@ -457,11 +419,6 @@ export const getSelectedBreakableLines = createSelector(
breakableLines => new Set(breakableLines || [])
);
export function isSourceLoadingOrLoaded(state, sourceId) {
const { content } = getResource(state.sources.sources, sourceId);
return content !== null;
}
export function getBlackBoxRanges(state) {
return state.sources.blackboxedRanges;
}

Просмотреть файл

@ -11,7 +11,7 @@ import {
getSpecificSourceByURL,
getSources,
resourceAsSourceBase,
} from "../selectors/sources";
} from "./sources";
import { isOriginalId } from "devtools-source-map";
import { isSimilarTab } from "../utils/tabs";

Просмотреть файл

@ -58,7 +58,8 @@ describe("visible column breakpoints", () => {
pausePoints,
breakpoints,
viewport,
source
source,
source.content
);
expect(columnBps).toMatchSnapshot();
});
@ -74,7 +75,8 @@ describe("visible column breakpoints", () => {
pausePoints,
breakpoints,
viewport,
source
source,
source.content
);
expect(columnBps).toMatchSnapshot();
});
@ -91,7 +93,8 @@ describe("visible column breakpoints", () => {
pausePoints,
breakpoints,
viewport,
source
source,
source.content
);
expect(columnBps).toMatchSnapshot();
});
@ -108,7 +111,8 @@ describe("visible column breakpoints", () => {
pausePoints,
breakpoints,
viewport,
source
source,
source.content
);
expect(columnBps).toMatchSnapshot();
});

Просмотреть файл

@ -5,7 +5,7 @@
import { createSelector } from "reselect";
import { getBreakpointsList } from "./breakpoints";
import { getSelectedSource } from "../selectors/sources";
import { getSelectedSource } from "./sources";
import { sortSelectedBreakpoints } from "../utils/breakpoint";
import { getSelectedLocation } from "../utils/selected-location";

Просмотреть файл

@ -8,10 +8,10 @@ import {
getViewport,
getSource,
getSelectedSource,
getSelectedSourceWithContent,
getSelectedSourceTextContent,
getBreakpointPositions,
getBreakpointPositionsForSource,
} from "../selectors";
} from "./index";
import { getVisibleBreakpoints } from "./visibleBreakpoints";
import { getSelectedLocation } from "../utils/selected-location";
import { sortSelectedLocations } from "../utils/location";
@ -126,7 +126,8 @@ export function getColumnBreakpoints(
positions,
breakpoints,
viewport,
selectedSource
selectedSource,
selectedSourceTextContent
) {
if (!positions || !selectedSource) {
return [];
@ -140,7 +141,11 @@ export function getColumnBreakpoints(
const breakpointMap = groupBreakpoints(breakpoints, selectedSource);
positions = filterByLineCount(positions, selectedSource);
positions = filterVisible(positions, selectedSource, viewport);
positions = filterInLine(positions, selectedSource, selectedSource.content);
positions = filterInLine(
positions,
selectedSource,
selectedSourceTextContent
);
positions = filterByBreakpoints(positions, selectedSource, breakpointMap);
return formatPositions(positions, selectedSource, breakpointMap);
@ -167,7 +172,8 @@ export const visibleColumnBreakpoints = createSelector(
getVisibleBreakpointPositions,
getVisibleBreakpoints,
getViewport,
getSelectedSourceWithContent,
getSelectedSource,
getSelectedSourceTextContent,
getColumnBreakpoints
);

Просмотреть файл

@ -122,7 +122,7 @@ function setMode(editor, source, content, symbols) {
// Disable modes for minified files with 1+ million characters Bug 1569829
if (
content.type === "text" &&
isMinified(source) &&
isMinified(source, content) &&
content.value.length > 1000000
) {
return;

Просмотреть файл

@ -6,7 +6,7 @@ import { isFulfilled } from "./async-value";
import { findClosestFunction } from "./ast";
import { correctIndentation } from "./indentation";
export function findFunctionText(line, source, symbols) {
export function findFunctionText(line, source, sourceTextContent, symbols) {
const func = findClosestFunction(symbols, {
sourceId: source.id,
line,
@ -16,9 +16,9 @@ export function findFunctionText(line, source, symbols) {
if (
source.isWasm ||
!func ||
!source.content ||
!isFulfilled(source.content) ||
source.content.value.type !== "text"
!sourceTextContent ||
!isFulfilled(sourceTextContent) ||
sourceTextContent.value.type !== "text"
) {
return null;
}
@ -26,7 +26,7 @@ export function findFunctionText(line, source, symbols) {
const {
location: { start, end },
} = func;
const lines = source.content.value.value.split("\n");
const lines = sourceTextContent.value.value.split("\n");
const firstLine = lines[start.line - 1].slice(start.column);
const lastLine = lines[end.line - 1].slice(0, end.column);
const middle = lines.slice(start.line, end.line - 1);

Просмотреть файл

@ -10,20 +10,20 @@ const INDENT_COUNT_THRESHOLD = 5;
const CHARACTER_LIMIT = 250;
const _minifiedCache = new Map();
export function isMinified(source) {
export function isMinified(source, sourceTextContent) {
if (_minifiedCache.has(source.id)) {
return _minifiedCache.get(source.id);
}
if (
!source.content ||
!isFulfilled(source.content) ||
source.content.value.type !== "text"
!sourceTextContent ||
!isFulfilled(sourceTextContent) ||
sourceTextContent.value.type !== "text"
) {
return false;
}
let text = source.content.value.value;
let text = sourceTextContent.value.value;
let lineEndIndex = 0;
let lineStartIndex = 0;

Просмотреть файл

@ -12,7 +12,7 @@ describe("function", () => {
it("finds function", () => {
const source = populateOriginalSource("func");
const symbols = getSymbols(source.id);
const text = findFunctionText(14, source, symbols);
const text = findFunctionText(14, source, source.content, symbols);
expect(text).toMatchSnapshot();
});
@ -20,7 +20,7 @@ describe("function", () => {
const source = populateOriginalSource("func");
const symbols = getSymbols(source.id);
const text = findFunctionText(13, source, symbols);
const text = findFunctionText(13, source, source.content, symbols);
expect(text).toMatchSnapshot();
});
@ -28,7 +28,7 @@ describe("function", () => {
const source = populateOriginalSource("func");
const symbols = getSymbols(source.id);
const text = findFunctionText(15, source, symbols);
const text = findFunctionText(15, source, source.content, symbols);
// TODO: we should try and match the closing bracket.
expect(text).toEqual(null);
@ -38,7 +38,7 @@ describe("function", () => {
const source = populateOriginalSource("func");
const symbols = getSymbols(source.id);
const text = findFunctionText(29, source, symbols);
const text = findFunctionText(29, source, source.content, symbols);
expect(text).toMatchSnapshot();
});
@ -46,7 +46,7 @@ describe("function", () => {
const source = populateOriginalSource("func");
const symbols = getSymbols(source.id);
const text = findFunctionText(33, source, symbols);
const text = findFunctionText(33, source, source.content, symbols);
expect(text).toMatchSnapshot();
});
@ -54,7 +54,7 @@ describe("function", () => {
const source = populateOriginalSource("func");
const symbols = getSymbols(source.id);
const text = findFunctionText(20, source, symbols);
const text = findFunctionText(20, source, source.content, symbols);
expect(text).toEqual(null);
});
});

Просмотреть файл

@ -13,6 +13,6 @@ describe("isMinified", () => {
undefined,
"function base(boo) {\n}"
);
expect(isMinified(sourceWithContent)).toBe(true);
expect(isMinified(sourceWithContent, sourceWithContent.content)).toBe(true);
});
});

Просмотреть файл

@ -37,17 +37,13 @@ function getSourceContent(name, type = "js") {
}
export function getSource(name, type) {
return getSourceWithContent(name, type);
}
export function getSourceWithContent(name, type) {
const { value: text, contentType } = getSourceContent(name, type);
return makeMockSourceAndContent(undefined, name, contentType, text);
}
export function populateSource(name, type) {
const { content, ...source } = getSourceWithContent(name, type);
const { content, ...source } = getSource(name, type);
setSource({
id: source.id,
text: content.value,

Просмотреть файл

@ -19,7 +19,7 @@ add_task(async function testBreakableLinesOverReloads() {
);
info("Assert breakable lines of the first html page load");
await assertBreakableLines(dbg, "index.html", 20, [[17], [18]]);
await assertBreakableLines(dbg, "index.html", 22, [[17], [18]]);
info("Assert breakable lines of the first original source file, original.js");
// The length of original.js is longer than the test file
@ -74,11 +74,8 @@ function shouldLineBeBreakable(breakableLines, line) {
async function assertBreakableLines(dbg, file, numberOfLines, breakableLines) {
await selectSource(dbg, file);
const editorLines = dbg.win.document.querySelectorAll(
".CodeMirror-lines .CodeMirror-code > div"
);
is(
editorLines.length,
getCM(dbg).lineCount(),
numberOfLines,
`We show the expected number of lines in CodeMirror for ${file}`
);

Просмотреть файл

@ -60,7 +60,7 @@ async function invokeAndAssertBreakpoints(dbg) {
function assertPauseLocation(dbg, line, url = "event-breakpoints.js") {
const { location } = dbg.selectors.getVisibleSelectedFrame();
const selectedSource = dbg.selectors.getSelectedSourceWithContent();
const selectedSource = dbg.selectors.getSelectedSource();
is(location.sourceId, selectedSource.id, `Correct selected sourceId`);
ok(selectedSource.url.includes(url), "Correct url");

Просмотреть файл

@ -13,8 +13,9 @@ add_task(async function() {
await selectSource(dbg, "pretty.js", 4, 8);
prettyPrint(dbg);
info("Wait for a second tab to be displayed with the pretty printed source");
await waitForTabCounts(dbg, 2);
await waitForElementWithSelector(dbg, selectors.prettyPrintLoader);
info("Wait for the pretty printed source to be selected on a different line");
await waitForSelectedLocation(dbg, 5);
});

Просмотреть файл

@ -23,7 +23,7 @@ add_task(async function() {
// We should be paused at the first line of simple-worker.js
// Each worker has its own sources, so we have to retrieve the new source,
// which has just been opened on pause
const workerSource2 = dbg.selectors.getSelectedSourceWithContent();
const workerSource2 = dbg.selectors.getSelectedSource();
assertPausedAtSourceAndLine(dbg, workerSource2.id, 1);
// We have to remove the first breakpoint, set on the fist worker.
// The first worker is loaded on the html page load.
@ -40,7 +40,7 @@ add_task(async function() {
await waitForPaused(dbg, "simple-worker.js");
// We should be paused in the message listener in simple-worker.js
const workerSource3 = dbg.selectors.getSelectedSourceWithContent();
const workerSource3 = dbg.selectors.getSelectedSource();
assertPausedAtSourceAndLine(dbg, workerSource3.id, 10);
await removeBreakpoint(dbg, workerSource2.id, 10, 2);
});

Просмотреть файл

@ -37,7 +37,7 @@ add_task(async function() {
await dbg.actions.breakOnNext(getThreadContext(dbg));
await waitForPaused(dbg, "simple-worker.js");
threadIsSelected(dbg, 2);
const workerSource2 = dbg.selectors.getSelectedSourceWithContent();
const workerSource2 = dbg.selectors.getSelectedSource();
assertPausedAtSourceAndLine(dbg, workerSource2.id, 3);
info("Add a watch expression and view the value");
@ -90,7 +90,7 @@ add_task(async function() {
await dbg.actions.selectThread(getContext(dbg), thread2);
threadIsSelected(dbg, 3);
await waitForPaused(dbg);
const workerSource3 = dbg.selectors.getSelectedSourceWithContent();
const workerSource3 = dbg.selectors.getSelectedSource();
assertPausedAtSourceAndLine(dbg, workerSource3.id, 10);
info("StepOver in second worker and not the first");

Просмотреть файл

@ -21,7 +21,7 @@ add_task(async function() {
await onRemoved;
// We should be paused at the first line of simple-worker.js
const workerSource2 = dbg.selectors.getSelectedSourceWithContent();
const workerSource2 = dbg.selectors.getSelectedSource();
assertPausedAtSourceAndLine(dbg, workerSource2.id, 11);
await toggleNode(dbg, "var_array");

Просмотреть файл

@ -197,7 +197,8 @@ function waitForSelectedLocation(dbg, line, column) {
*/
function waitForSelectedSource(dbg, sourceOrUrl) {
const {
getSelectedSourceWithContent,
getSelectedSource,
getSelectedSourceTextContent,
hasSymbols,
getBreakableLines,
} = dbg.selectors;
@ -205,8 +206,9 @@ function waitForSelectedSource(dbg, sourceOrUrl) {
return waitForState(
dbg,
state => {
const source = getSelectedSourceWithContent() || {};
if (!source.content) {
const source = getSelectedSource() || {};
const sourceTextContent = getSelectedSourceTextContent();
if (!sourceTextContent) {
return false;
}
@ -259,8 +261,9 @@ function assertPausedLocation(dbg) {
function assertDebugLine(dbg, line, column) {
// Check the debug line
const lineInfo = getCM(dbg).lineInfo(line - 1);
const source = dbg.selectors.getSelectedSourceWithContent() || {};
if (source && !source.content) {
const source = dbg.selectors.getSelectedSource();
const sourceTextContent = dbg.selectors.getSelectedSourceTextContent();
if (source && !sourceTextContent) {
const url = source.url;
ok(
false,
@ -538,9 +541,10 @@ function isSelectedFrameSelected(dbg, state) {
// Make sure the source text is completely loaded for the
// source we are paused in.
const sourceId = frame.location.sourceId;
const source = dbg.selectors.getSelectedSourceWithContent() || {};
const source = dbg.selectors.getSelectedSource();
const sourceTextContent = dbg.selectors.getSelectedSourceTextContent();
if (!source || !source.content) {
if (!source || !sourceTextContent) {
return false;
}
@ -1475,7 +1479,6 @@ const selectors = {
replayNext: ".replay-next.active",
toggleBreakpoints: ".breakpoints-toggle",
prettyPrintButton: ".source-footer .prettyPrint",
prettyPrintLoader: ".source-footer .spin",
sourceMapLink: ".source-footer .mapped-source",
sourcesFooter: ".sources-panel .source-footer",
editorFooter: ".editor-pane .source-footer",

Просмотреть файл

@ -179,8 +179,14 @@ function selectSource(dbg, url) {
return waitForState(
dbg,
state => {
const source = dbg.selectors.getSelectedSourceWithContent(state);
if (!source || !source.content) {
const source = dbg.selectors.getSelectedSource(state);
if (!source) {
return false;
}
const sourceTextContent = dbg.selectors.getSelectedSourceTextContent(
state
);
if (!sourceTextContent) {
return false;
}