diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js')
-rw-r--r-- | Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js b/Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js new file mode 100644 index 000000000..2fa0c936f --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.FrameResourceManager = class FrameResourceManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.PageAgent) + PageAgent.enable(); + if (window.NetworkAgent) + NetworkAgent.enable(); + + WebInspector.notifications.addEventListener(WebInspector.Notification.ExtraDomainsActivated, this._extraDomainsActivated, this); + + this.initialize(); + } + + // Public + + initialize() + { + var oldMainFrame = this._mainFrame; + + this._frameIdentifierMap = new Map; + this._mainFrame = null; + this._resourceRequestIdentifierMap = new Map; + this._orphanedResources = new Map; + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + + this._waitingForMainFrameResourceTreePayload = true; + if (window.PageAgent) + PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this)); + } + + get mainFrame() + { + return this._mainFrame; + } + + get frames() + { + return [...this._frameIdentifierMap.values()]; + } + + frameForIdentifier(frameId) + { + return this._frameIdentifierMap.get(frameId) || null; + } + + frameDidNavigate(framePayload) + { + // Called from WebInspector.PageObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + var frameWasLoadedInstantly = false; + + var frame = this.frameForIdentifier(framePayload.id); + if (!frame) { + // If the frame wasn't known before now, then the main resource was loaded instantly (about:blank, etc.) + // Make a new resource (which will make the frame). Mark will mark it as loaded at the end too since we + // don't expect any more events about the load finishing for these frames. + var frameResource = this._addNewResourceToFrameOrTarget(null, framePayload.id, framePayload.loaderId, framePayload.url, null, null, null, null, null, framePayload.name, framePayload.securityOrigin); + frame = frameResource.parentFrame; + frameWasLoadedInstantly = true; + + console.assert(frame); + if (!frame) + return; + } + + if (framePayload.loaderId === frame.provisionalLoaderIdentifier) { + // There was a provisional load in progress, commit it. + frame.commitProvisionalLoad(framePayload.securityOrigin); + } else { + if (frame.mainResource.url !== framePayload.url || frame.loaderIdentifier !== framePayload.loaderId) { + // Navigations like back/forward do not have provisional loads, so create a new main resource here. + var mainResource = new WebInspector.Resource(framePayload.url, framePayload.mimeType, null, framePayload.loaderId); + } else { + // The main resource is already correct, so reuse it. + var mainResource = frame.mainResource; + } + + frame.initialize(framePayload.name, framePayload.securityOrigin, framePayload.loaderId, mainResource); + } + + var oldMainFrame = this._mainFrame; + + if (framePayload.parentId) { + var parentFrame = this.frameForIdentifier(framePayload.parentId); + console.assert(parentFrame); + + if (frame === this._mainFrame) + this._mainFrame = null; + + if (frame.parentFrame !== parentFrame) + parentFrame.addChildFrame(frame); + } else { + if (frame.parentFrame) + frame.parentFrame.removeChildFrame(frame); + this._mainFrame = frame; + } + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + + if (frameWasLoadedInstantly) + frame.mainResource.markAsFinished(); + } + + frameDidDetach(frameId) + { + // Called from WebInspector.PageObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + var frame = this.frameForIdentifier(frameId); + if (!frame) + return; + + if (frame.parentFrame) + frame.parentFrame.removeChildFrame(frame); + + this._frameIdentifierMap.delete(frame.id); + + var oldMainFrame = this._mainFrame; + + if (frame === this._mainFrame) + this._mainFrame = null; + + frame.clearExecutionContexts(); + + this.dispatchEventToListeners(WebInspector.FrameResourceManager.Event.FrameWasRemoved, {frame}); + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + } + + resourceRequestWillBeSent(requestIdentifier, frameIdentifier, loaderIdentifier, request, type, redirectResponse, timestamp, initiator, targetId) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + // COMPATIBILITY (iOS 8): Timeline timestamps for legacy backends are computed + // dynamically from the first backend timestamp received. For navigations we + // need to reset that base timestamp, and an appropriate timestamp to use is + // the new main resource's will be sent timestamp. So save this value on the + // resource in case it becomes a main resource. + var originalRequestWillBeSentTimestamp = timestamp; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + if (resource) { + // This is an existing request which is being redirected, update the resource. + console.assert(redirectResponse); + console.assert(!targetId); + resource.updateForRedirectResponse(request.url, request.headers, elapsedTime); + return; + } + + var initiatorSourceCodeLocation = this._initiatorSourceCodeLocationFromPayload(initiator); + + // This is a new request, make a new resource and add it to the right frame. + resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, request.url, type, request.method, request.headers, request.postData, elapsedTime, null, null, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId); + + // Associate the resource with the requestIdentifier so it can be found in future loading events. + this._resourceRequestIdentifierMap.set(requestIdentifier, resource); + } + + markResourceRequestAsServedFromMemoryCache(requestIdentifier) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + + // We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called). + // We don't want to assert in this case since we do likely have the resource, via PageAgent.getResourceTree. The Resource + // just doesn't have a requestIdentifier for us to look it up. + if (!resource) + return; + + resource.markAsCached(); + } + + resourceRequestWasServedFromMemoryCache(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload, timestamp, initiator) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + console.assert(!this._resourceRequestIdentifierMap.has(requestIdentifier)); + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + var initiatorSourceCodeLocation = this._initiatorSourceCodeLocationFromPayload(initiator); + var response = cachedResourcePayload.response; + var resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload.url, cachedResourcePayload.type, "GET", null, null, elapsedTime, null, null, initiatorSourceCodeLocation); + resource.markAsCached(); + resource.updateForResponse(cachedResourcePayload.url, response.mimeType, cachedResourcePayload.type, response.headers, response.status, response.statusText, elapsedTime, response.timing); + resource.increaseSize(cachedResourcePayload.bodySize, elapsedTime); + resource.increaseTransferSize(cachedResourcePayload.bodySize); + resource.markAsFinished(elapsedTime); + + if (cachedResourcePayload.sourceMapURL) + WebInspector.sourceMapManager.downloadSourceMap(cachedResourcePayload.sourceMapURL, resource.url, resource); + + // No need to associate the resource with the requestIdentifier, since this is the only event + // sent for memory cache resource loads. + } + + resourceRequestDidReceiveResponse(requestIdentifier, frameIdentifier, loaderIdentifier, type, response, timestamp) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + + // We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called). + // We don't want to assert in this case since we do likely have the resource, via PageAgent.getResourceTree. The Resource + // just doesn't have a requestIdentifier for us to look it up, but we can try to look it up by its URL. + if (!resource) { + var frame = this.frameForIdentifier(frameIdentifier); + if (frame) + resource = frame.resourceForURL(response.url); + + // If we find the resource this way we had marked it earlier as finished via PageAgent.getResourceTree. + // Associate the resource with the requestIdentifier so it can be found in future loading events. + // and roll it back to an unfinished state, we know now it is still loading. + if (resource) { + this._resourceRequestIdentifierMap.set(requestIdentifier, resource); + resource.revertMarkAsFinished(); + } + } + + // If we haven't found an existing Resource by now, then it is a resource that was loading when the inspector + // opened and we just missed the resourceRequestWillBeSent for it. So make a new resource and add it. + if (!resource) { + resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, response.url, type, null, response.requestHeaders, null, elapsedTime, null, null, null); + + // Associate the resource with the requestIdentifier so it can be found in future loading events. + this._resourceRequestIdentifierMap.set(requestIdentifier, resource); + } + + if (response.fromDiskCache) + resource.markAsCached(); + + resource.updateForResponse(response.url, response.mimeType, type, response.headers, response.status, response.statusText, elapsedTime, response.timing); + } + + resourceRequestDidReceiveData(requestIdentifier, dataLength, encodedDataLength, timestamp) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + + // We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called). + // We don't want to assert in this case since we do likely have the resource, via PageAgent.getResourceTree. The Resource + // just doesn't have a requestIdentifier for us to look it up. + if (!resource) + return; + + resource.increaseSize(dataLength, elapsedTime); + + if (encodedDataLength !== -1) + resource.increaseTransferSize(encodedDataLength); + } + + resourceRequestDidFinishLoading(requestIdentifier, timestamp, sourceMapURL) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + // By now we should always have the Resource. Either it was fetched when the inspector first opened with + // PageAgent.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse. + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + console.assert(resource); + if (!resource) + return; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + resource.markAsFinished(elapsedTime); + + if (sourceMapURL) + WebInspector.sourceMapManager.downloadSourceMap(sourceMapURL, resource.url, resource); + + this._resourceRequestIdentifierMap.delete(requestIdentifier); + } + + resourceRequestDidFailLoading(requestIdentifier, canceled, timestamp) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + // By now we should always have the Resource. Either it was fetched when the inspector first opened with + // PageAgent.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse. + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + console.assert(resource); + if (!resource) + return; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + resource.markAsFailed(canceled, elapsedTime); + + if (resource === resource.parentFrame.provisionalMainResource) + resource.parentFrame.clearProvisionalLoad(); + + this._resourceRequestIdentifierMap.delete(requestIdentifier); + } + + executionContextCreated(contextPayload) + { + // Called from WebInspector.RuntimeObserver. + + var frame = this.frameForIdentifier(contextPayload.frameId); + console.assert(frame); + if (!frame) + return; + + var displayName = contextPayload.name || frame.mainResource.displayName; + var executionContext = new WebInspector.ExecutionContext(WebInspector.mainTarget, contextPayload.id, displayName, contextPayload.isPageContext, frame); + frame.addExecutionContext(executionContext); + } + + resourceForURL(url) + { + if (!this._mainFrame) + return null; + + if (this._mainFrame.mainResource.url === url) + return this._mainFrame.mainResource; + + return this._mainFrame.resourceForURL(url, true); + } + + adoptOrphanedResourcesForTarget(target) + { + let resources = this._orphanedResources.take(target.identifier); + if (!resources) + return; + + for (let resource of resources) + target.adoptResource(resource); + } + + // Private + + _addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, url, type, requestMethod, requestHeaders, requestData, elapsedTime, frameName, frameSecurityOrigin, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId) + { + console.assert(!this._waitingForMainFrameResourceTreePayload); + + let resource = null; + + let frame = this.frameForIdentifier(frameIdentifier); + if (frame) { + // This is a new request for an existing frame, which might be the main resource or a new resource. + if (frame.mainResource.url === url && frame.loaderIdentifier === loaderIdentifier) + resource = frame.mainResource; + else if (frame.provisionalMainResource && frame.provisionalMainResource.url === url && frame.provisionalLoaderIdentifier === loaderIdentifier) + resource = frame.provisionalMainResource; + else { + resource = new WebInspector.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp); + if (resource.target === WebInspector.mainTarget) + this._addResourceToFrame(frame, resource); + else if (resource.target) + resource.target.addResource(resource); + else + this._addOrphanedResource(resource, targetId); + } + } else { + // This is a new request for a new frame, which is always the main resource. + console.assert(!targetId); + resource = new WebInspector.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp); + frame = new WebInspector.Frame(frameIdentifier, frameName, frameSecurityOrigin, loaderIdentifier, resource); + this._frameIdentifierMap.set(frame.id, frame); + + // If we don't have a main frame, assume this is it. This can change later in + // frameDidNavigate when the parent frame is known. + if (!this._mainFrame) { + this._mainFrame = frame; + this._mainFrameDidChange(null); + } + + this._dispatchFrameWasAddedEvent(frame); + } + + console.assert(resource); + + return resource; + } + + _addResourceToFrame(frame, resource) + { + console.assert(!this._waitingForMainFrameResourceTreePayload); + if (this._waitingForMainFrameResourceTreePayload) + return; + + console.assert(frame); + console.assert(resource); + + if (resource.loaderIdentifier !== frame.loaderIdentifier && !frame.provisionalLoaderIdentifier) { + // This is the start of a provisional load which happens before frameDidNavigate is called. + // This resource will be the new mainResource if frameDidNavigate is called. + frame.startProvisionalLoad(resource); + return; + } + + // This is just another resource, either for the main loader or the provisional loader. + console.assert(resource.loaderIdentifier === frame.loaderIdentifier || resource.loaderIdentifier === frame.provisionalLoaderIdentifier); + frame.addResource(resource); + } + + _addResourceToTarget(target, resource) + { + console.assert(target !== WebInspector.mainTarget); + console.assert(resource); + + target.addResource(resource); + } + + _initiatorSourceCodeLocationFromPayload(initiatorPayload) + { + if (!initiatorPayload) + return null; + + var url = null; + var lineNumber = NaN; + var columnNumber = 0; + + if (initiatorPayload.stackTrace && initiatorPayload.stackTrace.length) { + var stackTracePayload = initiatorPayload.stackTrace; + for (var i = 0; i < stackTracePayload.length; ++i) { + var callFramePayload = stackTracePayload[i]; + if (!callFramePayload.url || callFramePayload.url === "[native code]") + continue; + + url = callFramePayload.url; + + // The lineNumber is 1-based, but we expect 0-based. + lineNumber = callFramePayload.lineNumber - 1; + + columnNumber = callFramePayload.columnNumber; + + break; + } + } else if (initiatorPayload.url) { + url = initiatorPayload.url; + + // The lineNumber is 1-based, but we expect 0-based. + lineNumber = initiatorPayload.lineNumber - 1; + } + + if (!url || isNaN(lineNumber) || lineNumber < 0) + return null; + + var sourceCode = WebInspector.frameResourceManager.resourceForURL(url); + if (!sourceCode) + sourceCode = WebInspector.debuggerManager.scriptsForURL(url, WebInspector.mainTarget)[0]; + + if (!sourceCode) + return null; + + return sourceCode.createSourceCodeLocation(lineNumber, columnNumber); + } + + _processMainFrameResourceTreePayload(error, mainFramePayload) + { + console.assert(this._waitingForMainFrameResourceTreePayload); + delete this._waitingForMainFrameResourceTreePayload; + + if (error) { + console.error(JSON.stringify(error)); + return; + } + + console.assert(mainFramePayload); + console.assert(mainFramePayload.frame); + + this._resourceRequestIdentifierMap = new Map; + this._frameIdentifierMap = new Map; + + var oldMainFrame = this._mainFrame; + + this._mainFrame = this._addFrameTreeFromFrameResourceTreePayload(mainFramePayload, true); + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + } + + _createFrame(payload) + { + // If payload.url is missing or empty then this page is likely the special empty page. In that case + // we will just say it is "about:blank" so we have a URL, which is required for resources. + var mainResource = new WebInspector.Resource(payload.url || "about:blank", payload.mimeType, null, payload.loaderId); + var frame = new WebInspector.Frame(payload.id, payload.name, payload.securityOrigin, payload.loaderId, mainResource); + + this._frameIdentifierMap.set(frame.id, frame); + + mainResource.markAsFinished(); + + return frame; + } + + _createResource(payload, framePayload) + { + var resource = new WebInspector.Resource(payload.url, payload.mimeType, payload.type, framePayload.loaderId, payload.targetId); + + if (payload.sourceMapURL) + WebInspector.sourceMapManager.downloadSourceMap(payload.sourceMapURL, resource.url, resource); + + return resource; + } + + _addFrameTreeFromFrameResourceTreePayload(payload, isMainFrame) + { + var frame = this._createFrame(payload.frame); + if (isMainFrame) + frame.markAsMainFrame(); + + for (var i = 0; payload.childFrames && i < payload.childFrames.length; ++i) + frame.addChildFrame(this._addFrameTreeFromFrameResourceTreePayload(payload.childFrames[i], false)); + + for (var i = 0; payload.resources && i < payload.resources.length; ++i) { + var resourcePayload = payload.resources[i]; + + // The main resource is included as a resource. We can skip it since we already created + // a main resource when we created the Frame. The resource payload does not include anything + // didn't already get from the frame payload. + if (resourcePayload.type === "Document" && resourcePayload.url === payload.frame.url) + continue; + + var resource = this._createResource(resourcePayload, payload); + if (resource.target === WebInspector.mainTarget) + frame.addResource(resource); + else if (resource.target) + resource.target.addResource(resource); + else + this._addOrphanedResource(resource, resourcePayload.targetId); + + if (resourcePayload.failed || resourcePayload.canceled) + resource.markAsFailed(resourcePayload.canceled); + else + resource.markAsFinished(); + } + + this._dispatchFrameWasAddedEvent(frame); + + return frame; + } + + _addOrphanedResource(resource, targetId) + { + let resources = this._orphanedResources.get(targetId); + if (!resources) { + resources = []; + this._orphanedResources.set(targetId, resources); + } + + resources.push(resource); + } + + _dispatchFrameWasAddedEvent(frame) + { + this.dispatchEventToListeners(WebInspector.FrameResourceManager.Event.FrameWasAdded, {frame}); + } + + _mainFrameDidChange(oldMainFrame) + { + if (oldMainFrame) + oldMainFrame.unmarkAsMainFrame(); + if (this._mainFrame) + this._mainFrame.markAsMainFrame(); + + this.dispatchEventToListeners(WebInspector.FrameResourceManager.Event.MainFrameDidChange, {oldMainFrame}); + } + + _extraDomainsActivated(event) + { + if (event.data.domains.includes("Page") && window.PageAgent) + PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this)); + } +}; + +WebInspector.FrameResourceManager.Event = { + FrameWasAdded: "frame-resource-manager-frame-was-added", + FrameWasRemoved: "frame-resource-manager-frame-was-removed", + MainFrameDidChange: "frame-resource-manager-main-frame-did-change", +}; |