Enhance logging with detailed timing information for API and session operations

- Added timing logs to various functions in `ApiContext`, `ServeSession`, and `Reconciler` to track performance and improve debugging.
- Introduced detailed trace logs for connection, reading, writing, and patch application processes, providing insights into execution times.
- Updated the `App` component to log the patch tree building process, enhancing visibility during session startup.

These changes aim to improve performance monitoring and facilitate troubleshooting within the application.
This commit is contained in:
Joe 2026-03-01 14:07:47 -06:00
parent a0e07bb542
commit 7b9d44953b
6 changed files with 223 additions and 15 deletions

View file

@ -164,9 +164,22 @@ end
function ApiContext:connect() function ApiContext:connect()
local url = ("%s/api/rojo"):format(self.__baseUrl) local url = ("%s/api/rojo"):format(self.__baseUrl)
local connectClock = os.clock()
Log.trace("[TIMING] ApiContext:connect() GET {} starting", url)
return Http.get(url) return Http.get(url)
:andThen(function(response)
Log.trace(
"[TIMING] ApiContext:connect() HTTP response received ({:.1} ms)",
(os.clock() - connectClock) * 1000
)
return response
end)
:andThen(rejectFailedRequests) :andThen(rejectFailedRequests)
:andThen(Http.Response.msgpack) :andThen(Http.Response.msgpack)
:andThen(function(body)
Log.trace("[TIMING] ApiContext:connect() msgpack decoded")
return body
end)
:andThen(rejectWrongVersion) :andThen(rejectWrongVersion)
:andThen(rejectWrongFork) :andThen(rejectWrongFork)
:andThen(function(body) :andThen(function(body)
@ -177,6 +190,11 @@ function ApiContext:connect()
:andThen(rejectWrongPlaceId) :andThen(rejectWrongPlaceId)
:andThen(function(body) :andThen(function(body)
self.__sessionId = body.sessionId self.__sessionId = body.sessionId
Log.trace(
"[TIMING] ApiContext:connect() fully validated ({:.1} ms), sessionId={}",
(os.clock() - connectClock) * 1000,
body.sessionId
)
return body return body
end) end)
@ -185,12 +203,36 @@ end
function ApiContext:read(ids) function ApiContext:read(ids)
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ",")) local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body) local readClock = os.clock()
Log.trace("[TIMING] ApiContext:read() GET {} starting ({} ids)", url, #ids)
return Http.get(url)
:andThen(function(response)
Log.trace(
"[TIMING] ApiContext:read() HTTP response received ({:.1} ms, {} bytes)",
(os.clock() - readClock) * 1000,
if response.body then #response.body else 0
)
return response
end)
:andThen(rejectFailedRequests)
:andThen(function(response)
local decodeClock = os.clock()
Log.trace("[TIMING] ApiContext:read() msgpack decode starting")
return Http.Response.msgpack(response):andThen(function(body)
Log.trace(
"[TIMING] ApiContext:read() msgpack decode completed ({:.1} ms)",
(os.clock() - decodeClock) * 1000
)
return body
end)
end)
:andThen(function(body)
if body.sessionId ~= self.__sessionId then if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID") return Promise.reject("Server changed ID")
end end
assert(validateApiRead(body)) assert(validateApiRead(body))
Log.trace("[TIMING] ApiContext:read() fully completed ({:.1} ms)", (os.clock() - readClock) * 1000)
return body return body
end) end)
@ -282,11 +324,18 @@ function ApiContext:connectWebSocket(packetHandlers)
-- Convert HTTP/HTTPS URL to WS/WSS -- Convert HTTP/HTTPS URL to WS/WSS
url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://") url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://")
Log.trace("[TIMING] ApiContext:connectWebSocket() creating client for {}", url)
return Promise.new(function(resolve, reject) return Promise.new(function(resolve, reject)
local wsClock = os.clock()
local success, wsClient = local success, wsClient =
pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, { pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, {
Url = url, Url = url,
}) })
Log.trace(
"[TIMING] WebSocket CreateWebStreamClient ({:.1} ms, success={})",
(os.clock() - wsClock) * 1000,
success
)
if not success then if not success then
reject("Failed to create WebSocket client: " .. tostring(wsClient)) reject("Failed to create WebSocket client: " .. tostring(wsClient))
return return

View file

@ -1002,14 +1002,14 @@ function App:startSession()
self:setState({ self:setState({
connectingText = "Computing diff view...", connectingText = "Computing diff view...",
}) })
local patchTreeClock = os.clock()
Log.trace("[TIMING] PatchTree.build() starting")
local patchTree =
PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, serverInfo.gitMetadata)
Log.trace("[TIMING] PatchTree.build() completed ({:.1} ms)", (os.clock() - patchTreeClock) * 1000)
self:setState({ self:setState({
appStatus = AppStatus.Confirming, appStatus = AppStatus.Confirming,
patchTree = PatchTree.build( patchTree = patchTree,
patch,
instanceMap,
{ "Property", "Current", "Incoming" },
serverInfo.gitMetadata
),
confirmData = { confirmData = {
serverInfo = serverInfo, serverInfo = serverInfo,
}, },
@ -1021,6 +1021,7 @@ function App:startSession()
timeout = 7, timeout = 7,
}) })
Log.trace("[TIMING] Waiting for user to confirm/reject patch...")
local result = self.confirmationEvent:Wait() local result = self.confirmationEvent:Wait()
-- One-shot sync: Don't transition to Connected UI -- One-shot sync: Don't transition to Connected UI

View file

@ -19,6 +19,8 @@ local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferre
local setProperty = require(script.Parent.setProperty) local setProperty = require(script.Parent.setProperty)
local function applyPatch(instanceMap, patch) local function applyPatch(instanceMap, patch)
local applyPatchClock = os.clock()
-- Tracks any portions of the patch that could not be applied to the DOM. -- Tracks any portions of the patch that could not be applied to the DOM.
local unappliedPatch = PatchSet.newEmpty() local unappliedPatch = PatchSet.newEmpty()
@ -40,6 +42,14 @@ local function applyPatch(instanceMap, patch)
) )
end end
Log.trace(
"[TIMING] applyPatch() starting: {} removals, {} additions, {} updates",
#patch.removed,
addCount,
#patch.updated
)
local removalClock = os.clock()
for _, removedIdOrInstance in ipairs(patch.removed) do for _, removedIdOrInstance in ipairs(patch.removed) do
-- Skip auto-created instances that shouldn't be synced -- Skip auto-created instances that shouldn't be synced
local instance = if Types.RbxId(removedIdOrInstance) local instance = if Types.RbxId(removedIdOrInstance)
@ -67,6 +77,8 @@ local function applyPatch(instanceMap, patch)
end end
end end
Log.trace("[TIMING] applyPatch() removals completed ({:.1} ms)", (os.clock() - removalClock) * 1000)
-- Detect echoed additions from DescendantAdded handler. -- Detect echoed additions from DescendantAdded handler.
-- When the plugin pre-inserts instances with temp IDs and the server -- When the plugin pre-inserts instances with temp IDs and the server
-- broadcasts the same instances with real Refs, remap the InstanceMap -- broadcasts the same instances with real Refs, remap the InstanceMap
@ -100,6 +112,8 @@ local function applyPatch(instanceMap, patch)
end end
end end
local additionClock = os.clock()
Log.trace("[TIMING] applyPatch() additions starting")
for id, virtualInstance in pairs(patch.added) do for id, virtualInstance in pairs(patch.added) do
if instanceMap.fromIds[id] ~= nil then if instanceMap.fromIds[id] ~= nil then
-- This instance already exists. We might've already added it in a -- This instance already exists. We might've already added it in a
@ -146,6 +160,10 @@ local function applyPatch(instanceMap, patch)
end end
end end
Log.trace("[TIMING] applyPatch() additions completed ({:.1} ms)", (os.clock() - additionClock) * 1000)
local updateClock = os.clock()
Log.trace("[TIMING] applyPatch() updates starting ({} updates)", #patch.updated)
for _, update in ipairs(patch.updated) do for _, update in ipairs(patch.updated) do
local instance = instanceMap.fromIds[update.id] local instance = instanceMap.fromIds[update.id]
@ -314,7 +332,14 @@ local function applyPatch(instanceMap, patch)
end end
end end
Log.trace("[TIMING] applyPatch() updates completed ({:.1} ms)", (os.clock() - updateClock) * 1000)
local refsClock = os.clock()
Log.trace("[TIMING] applyPatch() deferred refs starting ({} refs)", #deferredRefs)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch) applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
Log.trace("[TIMING] applyPatch() deferred refs completed ({:.1} ms)", (os.clock() - refsClock) * 1000)
Log.trace("[TIMING] applyPatch() total completed ({:.1} ms)", (os.clock() - applyPatchClock) * 1000)
return unappliedPatch return unappliedPatch
end end

View file

@ -59,8 +59,15 @@ local function diff(instanceMap, virtualInstances, rootId, serverInfo)
updated = {}, updated = {},
} }
local diffClock = os.clock()
local diffCount = 0 local diffCount = 0
local virtualCount = 0
for _ in pairs(virtualInstances) do
virtualCount += 1
end
Log.trace("[TIMING] diff() starting ({} virtual instances to diff)", virtualCount)
-- Build a lookup table for visible services (for ignoreHiddenServices check) -- Build a lookup table for visible services (for ignoreHiddenServices check)
local visibleServicesSet: { [string]: boolean } = {} local visibleServicesSet: { [string]: boolean } = {}
if serverInfo and serverInfo.ignoreHiddenServices and serverInfo.visibleServices then if serverInfo and serverInfo.ignoreHiddenServices and serverInfo.visibleServices then
@ -247,6 +254,19 @@ local function diff(instanceMap, virtualInstances, rootId, serverInfo)
return false, err return false, err
end end
local addedCount = 0
for _ in pairs(patch.added) do
addedCount += 1
end
Log.trace(
"[TIMING] diff() completed: {} instances diffed, {} removals, {} additions, {} updates ({:.1} ms)",
diffCount,
#patch.removed,
addedCount,
#patch.updated,
(os.clock() - diffClock) * 1000
)
return true, patch return true, patch
end end

View file

@ -16,12 +16,15 @@ local Matching = require(script.Parent.matching)
local HYDRATE_YIELD_INTERVAL = 1000 local HYDRATE_YIELD_INTERVAL = 1000
local function hydrate(instanceMap, virtualInstances, rootId, rootInstance, session) local function hydrate(instanceMap, virtualInstances, rootId, rootInstance, session)
local isRoot = not session or not session.hydrateCount
if not session then if not session then
session = Matching.newSession() session = Matching.newSession()
end end
if not session.hydrateCount then if not session.hydrateCount then
session.hydrateCount = 0 session.hydrateCount = 0
session.hydrateClock = os.clock()
end end
session.hydrateCount += 1 session.hydrateCount += 1
if session.hydrateCount % HYDRATE_YIELD_INTERVAL == 0 then if session.hydrateCount % HYDRATE_YIELD_INTERVAL == 0 then
@ -34,6 +37,10 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance, sess
invariant("Cannot hydrate an instance not present in virtualInstances\nID: {}", rootId) invariant("Cannot hydrate an instance not present in virtualInstances\nID: {}", rootId)
end end
if isRoot then
Log.trace("[TIMING] hydrate() starting at root '{}' ({})", virtualInstance.Name, rootInstance:GetFullName())
end
instanceMap:insert(rootId, rootInstance) instanceMap:insert(rootId, rootInstance)
local existingChildren = rootInstance:GetChildren() local existingChildren = rootInstance:GetChildren()
@ -54,8 +61,21 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance, sess
end end
end end
local matchClock = os.clock()
local result = local result =
Matching.matchChildren(session, validVirtualIds, existingChildren, virtualInstances, rootId, rootInstance) Matching.matchChildren(session, validVirtualIds, existingChildren, virtualInstances, rootId, rootInstance)
local matchElapsed = (os.clock() - matchClock) * 1000
if matchElapsed > 10 then
Log.trace(
"[TIMING] matchChildren for '{}' took {:.1} ms ({} virtual, {} studio, {} matched)",
virtualInstance.Name,
matchElapsed,
#validVirtualIds,
#existingChildren,
#result.matched
)
end
-- Recursively hydrate matched pairs -- Recursively hydrate matched pairs
for _, pair in result.matched do for _, pair in result.matched do
@ -75,6 +95,15 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance, sess
) )
end end
end end
if isRoot then
Log.trace(
"[TIMING] hydrate() completed: {} instances processed, {} matched ({:.1} ms total)",
session.hydrateCount,
#result.matched,
(os.clock() - session.hydrateClock) * 1000
)
end
end end
return hydrate return hydrate

View file

@ -329,9 +329,13 @@ function ServeSession:start()
self:__setStatus(Status.Connecting) self:__setStatus(Status.Connecting)
self:setLoadingText("Connecting to server...") self:setLoadingText("Connecting to server...")
local sessionStartClock = os.clock()
Log.trace("[TIMING] ServeSession:start() begun")
self.__apiContext self.__apiContext
:connect() :connect()
:andThen(function(serverInfo) :andThen(function(serverInfo)
Log.trace("[TIMING] ApiContext:connect() completed ({:.1} ms)", (os.clock() - sessionStartClock) * 1000)
self:setLoadingText("Loading initial data from server...") self:setLoadingText("Loading initial data from server...")
-- Configure sync mode based on server capabilities -- Configure sync mode based on server capabilities
@ -342,9 +346,18 @@ function ServeSession:start()
self.__changeBatcher:setSyncScriptsOnly(self.__syncScriptsOnly) self.__changeBatcher:setSyncScriptsOnly(self.__syncScriptsOnly)
self.__serverInfo = serverInfo self.__serverInfo = serverInfo
local computePatchClock = os.clock()
Log.trace("[TIMING] __computeInitialPatch() starting")
return self:__computeInitialPatch(serverInfo):andThen(function(catchUpPatch) return self:__computeInitialPatch(serverInfo):andThen(function(catchUpPatch)
Log.trace(
"[TIMING] __computeInitialPatch() completed ({:.1} ms)",
(os.clock() - computePatchClock) * 1000
)
self:setLoadingText("Starting sync loop...") self:setLoadingText("Starting sync loop...")
local wsClock = os.clock()
Log.trace("[TIMING] connectWebSocket() starting")
-- Connect WebSocket BEFORE showing confirmation so changes -- Connect WebSocket BEFORE showing confirmation so changes
-- during confirmation can be merged into the patch -- during confirmation can be merged into the patch
-- Note: connectWebSocket returns a Promise that only resolves when -- Note: connectWebSocket returns a Promise that only resolves when
@ -360,12 +373,22 @@ function ServeSession:start()
self:__stopInternal(err) self:__stopInternal(err)
end end
end) end)
Log.trace("[TIMING] connectWebSocket() initiated ({:.1} ms)", (os.clock() - wsClock) * 1000)
local confirmClock = os.clock()
Log.trace("[TIMING] __confirmAndApplyInitialPatch() starting")
-- Now show confirmation and wait for user decision -- Now show confirmation and wait for user decision
return self:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo) return self:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo):andThen(function(...)
Log.trace(
"[TIMING] __confirmAndApplyInitialPatch() completed ({:.1} ms)",
(os.clock() - confirmClock) * 1000
)
return ...
end)
end) end)
end) end)
:andThen(function() :andThen(function()
Log.trace("[TIMING] ServeSession:start() total elapsed ({:.1} ms)", (os.clock() - sessionStartClock) * 1000)
-- Initial sync is fully complete (including any writes to the server) -- Initial sync is fully complete (including any writes to the server)
if self.__initialSyncCompleteCallback ~= nil then if self.__initialSyncCompleteCallback ~= nil then
self.__initialSyncCompleteCallback() self.__initialSyncCompleteCallback()
@ -568,7 +591,14 @@ function ServeSession:__applyPatch(patch)
Timer.stop() Timer.stop()
self.__applyingPatch = true self.__applyingPatch = true
local reconcilerClock = os.clock()
Log.trace("[TIMING] reconciler:applyPatch() starting")
local patchApplySuccess, unappliedPatch = pcall(self.__reconciler.applyPatch, self.__reconciler, patch) local patchApplySuccess, unappliedPatch = pcall(self.__reconciler.applyPatch, self.__reconciler, patch)
Log.trace(
"[TIMING] reconciler:applyPatch() completed ({:.1} ms, success={})",
(os.clock() - reconcilerClock) * 1000,
patchApplySuccess
)
if not patchApplySuccess then if not patchApplySuccess then
self.__applyingPatch = false self.__applyingPatch = false
if historyRecording then if historyRecording then
@ -579,8 +609,10 @@ function ServeSession:__applyPatch(patch)
error(unappliedPatch) error(unappliedPatch)
end end
local fallbackClock = os.clock()
local fallbackOk, fallbackErr = pcall(function() local fallbackOk, fallbackErr = pcall(function()
if Settings:get("enableSyncFallback") and not PatchSet.isEmpty(unappliedPatch) then if Settings:get("enableSyncFallback") and not PatchSet.isEmpty(unappliedPatch) then
Log.trace("[TIMING] Sync fallback starting (unapplied patch is non-empty)")
-- Some changes did not apply, let's try replacing them instead -- Some changes did not apply, let's try replacing them instead
local addedIdList = PatchSet.addedIdList(unappliedPatch) local addedIdList = PatchSet.addedIdList(unappliedPatch)
local updatedIdList = PatchSet.updatedIdList(unappliedPatch) local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
@ -607,6 +639,7 @@ function ServeSession:__applyPatch(patch)
end end
end) end)
self.__applyingPatch = false self.__applyingPatch = false
Log.trace("[TIMING] Sync fallback phase completed ({:.1} ms)", (os.clock() - fallbackClock) * 1000)
if not fallbackOk then if not fallbackOk then
Log.warn("Sync fallback errored: {}", fallbackErr) Log.warn("Sync fallback errored: {}", fallbackErr)
@ -638,7 +671,17 @@ function ServeSession:__applyPatch(patch)
end end
function ServeSession:__computeInitialPatch(serverInfo) function ServeSession:__computeInitialPatch(serverInfo)
local readClock = os.clock()
Log.trace("[TIMING] ApiContext:read() starting")
return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody) return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
Log.trace("[TIMING] ApiContext:read() completed ({:.1} ms)", (os.clock() - readClock) * 1000)
local instanceCount = 0
for _ in pairs(readResponseBody.instances) do
instanceCount += 1
end
Log.trace("[TIMING] Read response contains {} instances", instanceCount)
-- Tell the API Context that we're up-to-date with the version of -- Tell the API Context that we're up-to-date with the version of
-- the tree defined in this response. -- the tree defined in this response.
self.__apiContext:setMessageCursor(readResponseBody.messageCursor) self.__apiContext:setMessageCursor(readResponseBody.messageCursor)
@ -647,20 +690,43 @@ function ServeSession:__computeInitialPatch(serverInfo)
-- tracking them in the reconciler. -- tracking them in the reconciler.
Log.trace("Matching existing Roblox instances to Rojo IDs") Log.trace("Matching existing Roblox instances to Rojo IDs")
self:setLoadingText("Hydrating instance map...") self:setLoadingText("Hydrating instance map...")
local hydrateClock = os.clock()
Log.trace("[TIMING] reconciler:hydrate() starting ({} virtual instances)", instanceCount)
local matchingSession = Matching.newSession() local matchingSession = Matching.newSession()
self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game, matchingSession) self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game, matchingSession)
Log.trace("[TIMING] reconciler:hydrate() completed ({:.1} ms)", (os.clock() - hydrateClock) * 1000)
local hydratedCount = 0
for _ in pairs(self.__instanceMap.fromIds) do
hydratedCount += 1
end
Log.trace("[TIMING] InstanceMap contains {} entries after hydration", hydratedCount)
-- Calculate the initial patch to apply to the DataModel to catch us -- Calculate the initial patch to apply to the DataModel to catch us
-- up to what Rojo thinks the place should look like. -- up to what Rojo thinks the place should look like.
Log.trace("Computing changes that plugin needs to make to catch up to server...") Log.trace("Computing changes that plugin needs to make to catch up to server...")
self:setLoadingText("Finding differences between server and Studio...") self:setLoadingText("Finding differences between server and Studio...")
local diffClock = os.clock()
Log.trace("[TIMING] reconciler:diff() starting")
local success, catchUpPatch = local success, catchUpPatch =
self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, serverInfo) self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, serverInfo)
Log.trace("[TIMING] reconciler:diff() completed ({:.1} ms)", (os.clock() - diffClock) * 1000)
if not success then if not success then
Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch) Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch)
end end
local addedCount = 0
for _ in pairs(catchUpPatch.added) do
addedCount += 1
end
Log.trace(
"[TIMING] Diff result: {} removals, {} additions, {} updates",
#catchUpPatch.removed,
addedCount,
#catchUpPatch.updated
)
for _, update in catchUpPatch.updated do for _, update in catchUpPatch.updated do
if update.id == self.__instanceMap.fromInstances[game] and update.changedClassName ~= nil then if update.id == self.__instanceMap.fromInstances[game] and update.changedClassName ~= nil then
-- Non-place projects will try to update the classname of game from DataModel to -- Non-place projects will try to update the classname of game from DataModel to
@ -690,7 +756,14 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
self.__isConfirming = true self.__isConfirming = true
self.__confirmingPatch = catchUpPatch self.__confirmingPatch = catchUpPatch
local confirmWaitClock = os.clock()
Log.trace("[TIMING] Waiting for user confirmation...")
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo) userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo)
Log.trace(
"[TIMING] User confirmation received: {} ({:.1} ms wait)",
tostring(userDecision),
(os.clock() - confirmWaitClock) * 1000
)
-- Clear confirmation state flags -- Clear confirmation state flags
self.__confirmingPatch = nil self.__confirmingPatch = nil
@ -713,7 +786,10 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
-- Legacy: Accept all changes (push all to Studio) -- Legacy: Accept all changes (push all to Studio)
self:__setStatus(Status.Connected, serverInfo.projectName) self:__setStatus(Status.Connected, serverInfo.projectName)
self:__applyGameAndPlaceId(serverInfo) self:__applyGameAndPlaceId(serverInfo)
local applyPatchClock = os.clock()
Log.trace("[TIMING] __applyPatch() starting (Accept path)")
self:__applyPatch(catchUpPatch) self:__applyPatch(catchUpPatch)
Log.trace("[TIMING] __applyPatch() completed ({:.1} ms)", (os.clock() - applyPatchClock) * 1000)
return Promise.resolve() return Promise.resolve()
elseif type(userDecision) == "table" and userDecision.type == "Confirm" then elseif type(userDecision) == "table" and userDecision.type == "Confirm" then
-- New: Apply based on per-item selections -- New: Apply based on per-item selections
@ -881,7 +957,10 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
#pushPatch.removed, #pushPatch.removed,
#pushPatch.updated #pushPatch.updated
) )
local pushClock = os.clock()
Log.trace("[TIMING] __applyPatch() starting (Confirm push path)")
self:__applyPatch(pushPatch) self:__applyPatch(pushPatch)
Log.trace("[TIMING] __applyPatch() completed ({:.1} ms)", (os.clock() - pushClock) * 1000)
end end
if Settings:get("oneShotSync") then if Settings:get("oneShotSync") then
@ -908,7 +987,12 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
if hasStageIds then if hasStageIds then
Log.info("Requesting git stage for {} files", #stageIds) Log.info("Requesting git stage for {} files", #stageIds)
end end
return self.__apiContext:write(pullPatch, stageIds) local writeClock = os.clock()
Log.trace("[TIMING] ApiContext:write() starting (pull patch)")
return self.__apiContext:write(pullPatch, stageIds):andThen(function(...)
Log.trace("[TIMING] ApiContext:write() completed ({:.1} ms)", (os.clock() - writeClock) * 1000)
return ...
end)
end end
return Promise.resolve() return Promise.resolve()