Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 85 additions & 12 deletions invert-gradle-plugin/src/main/resources/META-INF/invert.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,89 @@ window.externalLoadJavaScriptFile = function (key, callback) {

/**
* Load a stat key via chunked JSON transport when a manifest is available.
* Falls back to legacy externalLoadJavaScriptFile if no manifest exists.
* Falls back to legacy externalLoadJavaScriptFile only when no manifest exists.
*
* Flow: fetch manifest → fetch all chunks in parallel → merge statsByModule → callback(json)
*/
window.externalLoadChunkedJsonFile = function (key, callback) {
window.externalLoadChunkedJsonFile = function (key, callback, errorCallback) {
function chunkTransportUnavailable(message) {
var err = new Error(message);
err.noChunkManifest = true;
return err;
}

function fetchWithTimeout(url, timeoutMs, responseHandler) {
var controller = typeof AbortController === "function" ? new AbortController() : null;
var timeoutId;
var timeout = new Promise(function (_, reject) {
timeoutId = setTimeout(function () {
if (controller) {
controller.abort();
}
reject(new Error("Timed out fetching " + url));
}, timeoutMs);
});
var request = Promise.resolve().then(function () {
return controller ? fetch(url, { signal: controller.signal }) : fetch(url);
}).then(function (response) {
return responseHandler ? responseHandler(response) : response;
});
return Promise.race([request, timeout]).then(
function (response) {
clearTimeout(timeoutId);
return response;
},
function (err) {
clearTimeout(timeoutId);
throw err;
}
);
}

function fetchJson(url, attemptsRemaining) {
return fetchWithTimeout(
url,
30000,
function (response) {
if (!response.ok) throw new Error("Failed to fetch " + url + " (" + response.status + ")");
return response.json();
}
)
.catch(function (err) {
if (attemptsRemaining > 1) {
return fetchJson(url, attemptsRemaining - 1);
}
throw err;
});
}

var manifestUrl = "js/" + key + ".manifest.json";
fetch(manifestUrl)
.then(function (response) {
if (!response.ok) throw new Error("No manifest for " + key);
return response.json();
fetchWithTimeout(
manifestUrl,
30000,
function (response) {
if (!response.ok) {
throw chunkTransportUnavailable("Manifest unavailable for " + key + " (" + response.status + ")");
}
return response.json().catch(function (err) {
err.invalidChunkManifest = true;
throw err;
});
}
)
.catch(function (err) {
if (err.noChunkManifest || err.invalidChunkManifest) {
throw err;
}
throw chunkTransportUnavailable("Unable to fetch manifest for " + key + ": " + err.message);
})
.then(function (manifest) {
if (!manifest.chunkFiles || manifest.chunkFiles.length === 0) {
throw new Error("Chunk manifest for " + key + " does not list any chunk files");
}
return Promise.all(
manifest.chunkFiles.map(function (chunkFile) {
return fetch("js/" + chunkFile).then(function (r) {
if (!r.ok) throw new Error("Failed to fetch chunk: " + chunkFile);
return r.json();
});
return fetchJson("js/" + chunkFile, 3);
})
);
})
Expand All @@ -103,8 +168,16 @@ window.externalLoadChunkedJsonFile = function (key, callback) {
callback(JSON.stringify(merged));
})
.catch(function (err) {
console.log("Chunked load unavailable for " + key + ", falling back to legacy: " + err.message);
externalLoadJavaScriptFile(key, callback);
if (err.noChunkManifest) {
console.log("Chunked load unavailable for " + key + ", falling back to legacy: " + err.message);
externalLoadJavaScriptFile(key, callback);
return;
}
var errorMessage = "Chunked load failed for " + key + ": " + err.message;
console.error(errorMessage);
if (typeof errorCallback === "function") {
errorCallback(errorMessage);
}
});
}

Expand Down
6 changes: 5 additions & 1 deletion invert-report/src/jsMain/kotlin/External.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

external fun externalLoadJavaScriptFile(key: String, callback: (json: String) -> Unit)

external fun externalLoadChunkedJsonFile(key: String, callback: (json: String) -> Unit)
external fun externalLoadChunkedJsonFile(
key: String,
callback: (json: String) -> Unit,
errorCallback: (message: String) -> Unit,
)

external fun loadJsFileAsync(url: String, callback: () -> Unit)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext

/**
Expand All @@ -32,6 +33,7 @@ class CollectedDataRepo(
) {

private val hasLoadedFile = mutableMapOf<FileKey, Boolean>()
private val _fileLoadFailures = MutableStateFlow<Map<FileKey, String>>(mapOf())

private suspend fun loadJsOfType(fileKey: FileKey) = withContext(coroutineDispatcher) {
if (!hasLoadedFile.contains(fileKey)) {
Expand All @@ -44,6 +46,27 @@ class CollectedDataRepo(
loadJsOfType(jsReportFileKey.key)
}

fun fileLoadFailure(fileKey: FileKey): Flow<String?> =
_fileLoadFailures.map { it[fileKey] }

fun fileLoadFailed(fileKey: FileKey, message: String) {
hasLoadedFile.remove(fileKey)
_fileLoadFailures.update {
it.toMutableMap().apply {
put(fileKey, message)
}
}
}

fun fileLoadSucceeded(fileKey: FileKey) {
hasLoadedFile[fileKey] = true
_fileLoadFailures.update {
it.toMutableMap().apply {
remove(fileKey)
}
}
}

private val _collectedPluginInfoReport: MutableStateFlow<PluginsJsReportModel?> =
MutableStateFlow(null)
val collectedPluginInfoReport: Flow<PluginsJsReportModel?> = _collectedPluginInfoReport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,21 @@ class InvertReport(
val collectedDataRepo = CollectedDataRepo(
coroutineDispatcher = Dispatchers.Default,
loadFileData = { jsFileKey, reportRepoData ->
RemoteJsLoadingProgress.loadJavaScriptFile(jsFileKey) { json ->
RemoteJsLoadingProgress.handleLoadedJsFile(reportRepoData, jsFileKey, json)
RemoteJsLoadingProgress.loadJavaScriptFile(
fileKey = jsFileKey,
onFailure = { message ->
reportRepoData.fileLoadFailed(jsFileKey, message)
}
) { json ->
try {
RemoteJsLoadingProgress.handleLoadedJsFile(reportRepoData, jsFileKey, json)
reportRepoData.fileLoadSucceeded(jsFileKey)
} catch (throwable: Throwable) {
reportRepoData.fileLoadFailed(
fileKey = jsFileKey,
message = throwable.message ?: throwable.toString()
)
}
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.squareup.invert.models.StatKey
import com.squareup.invert.models.StatMetadata
import com.squareup.invert.models.js.CollectedStatTotalsJsReportModel
import com.squareup.invert.models.js.HistoricalData
import com.squareup.invert.models.js.JsReportFileKey
import com.squareup.invert.models.js.MetadataJsReportModel
import com.squareup.invert.models.js.PluginsJsReportModel
import com.squareup.invert.models.js.StatJsReportModel
Expand Down Expand Up @@ -103,6 +104,9 @@ class ReportDataRepo(
fun statForKey(statKey: StatKey): Flow<StatJsReportModel?> =
collectedDataRepo.statData(statKey).mapLatest { it?.get(statKey) }

fun statLoadFailure(statKey: StatKey): Flow<String?> =
collectedDataRepo.fileLoadFailure(JsReportFileKey.STAT.key + "_$statKey")

val pluginIdToAllModulesMap: Flow<Map<GradlePluginId, List<ModulePath>>?> =
collectedDataRepo.collectedPluginInfoReport
.mapLatest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,15 @@ fun StatDetailComposable(
val metadata by reportDataRepo.reportMetadata.collectAsState(null)

val statKey = statsNavRoute.statKey
val statLoadFailure: String? by reportDataRepo.statLoadFailure(statKey).collectAsState(null)
val statsData: StatJsReportModel? by reportDataRepo.statForKey(statKey).collectAsState(null)

statLoadFailure?.let { message ->
H3 { Text("Failed to load stat data") }
Text(message)
return
}

if (moduleToOwnerMapFlowValue == null || metadata == null || statsData == null) {
BootstrapLoadingSpinner()
return
Expand Down Expand Up @@ -274,4 +281,3 @@ fun statToDetailsString(stat: Stat?): String = when (stat) {

else -> ""
} ?: ""

Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,63 @@ object RemoteJsLoadingProgress {
}
}

fun loadJavaScriptFile(fileKey: String, callback: (String) -> Unit) {
fun loadJavaScriptFile(
fileKey: String,
onFailure: (String) -> Unit = {},
callback: (String) -> Unit
) {
if (!awaitingResults.value.contains(fileKey)) {
awaitingResults.value = awaitingResults.value.toMutableList().apply { add(fileKey) }
Log.d("Loading $fileKey")
val loadingTimeout = window.setTimeout(
{
Log.d("Timeout while fetching $fileKey data.")
var loadingTimeout = 0
var completed = false
fun complete(block: () -> Unit) {
if (completed) return
completed = true
try {
block()
} finally {
window.clearTimeout(loadingTimeout)
awaitingResults.value = awaitingResults.value.toMutableList().apply { remove(fileKey) }
}
}
val onLoaded: (String) -> Unit = { json ->
complete {
Log.d("Finished Loading $fileKey")
callback(json)
}
}
val onFailed: (String) -> Unit = { message ->
complete {
Log.d("Failed Loading $fileKey: $message")
onFailure(message)
}
}
fun failureMessage(throwable: Throwable): String =
throwable.message ?: throwable.toString()

loadingTimeout = window.setTimeout(
{
onFailed("Timed out fetching $fileKey data.")
},
getTimeoutForFileKey(fileKey)
)
val onLoaded: (String) -> Unit = { json ->
Log.d("Finished Loading $fileKey")
window.clearTimeout(loadingTimeout)
callback(json)
awaitingResults.value = awaitingResults.value.toMutableList().apply { remove(fileKey) }
}
if (fileKey.startsWith("stat_")) {
externalLoadChunkedJsonFile(fileKey, onLoaded)
} else {
externalLoadJavaScriptFile(fileKey, onLoaded)
try {
if (fileKey.startsWith("stat_")) {
externalLoadChunkedJsonFile(fileKey, onLoaded, onFailed)
} else {
externalLoadJavaScriptFile(fileKey, onLoaded)
}
} catch (throwable: Throwable) {
if (fileKey.startsWith("stat_")) {
try {
externalLoadJavaScriptFile(fileKey, onLoaded)
} catch (legacyThrowable: Throwable) {
onFailed(failureMessage(legacyThrowable))
}
} else {
onFailed(failureMessage(throwable))
}
}
}
}
Expand Down Expand Up @@ -123,4 +159,4 @@ object RemoteJsLoadingProgress {
}
}
}
}
}
Loading