Customize UI Bundling Configurations for C3 AI Release Management
For some C3 AI packages, the UI bundling step takes a long time when you don’t optimize the hardware configuration. Vertically scaling the UI bundling step by itself can help speed up performance significantly in these cases, allowing faster build times.
Overwriting the UI bundling configuration is most useful when UI bundling is the longest step in your build pipeline or when bundling retries occur due to insufficient memory or node eviction.
You configure UI bundling in two places:
- The start environment spec, which specifies the desired hardware profile and availability for the bundling step.
- The UI bundling config, which specifies the concurrency level for the bundling batch job.
Some advanced bundling optimizations require environment capabilities that may not be enabled in all customer deployments. If these options are not available in your environment, contact your Solution Engineer.
Pre-requisite: Determine the right hardware profile for UI bundling
Before increasing maxConcurrencyPerNode, verify that your environment has enough CPU and memory to support additional parallel bundlers. You can inspect the hardware profile used for UI bundling by checking the node pool configuration:
// View all node pools in the environment
C3.app().nodePools();
// Inspect the configuration for the node pool used by UI bundling
// (typically "singlenode" or "task", depending on your deployment)
C3.app().nodePool('<node-pool-name>').config();The hardware profile lists available CPU cores, memory (memoryMb), and maxConcurrentComputes. Each UI bundler process typically requires 8–12 GB of memory, depending on the size of the UI codebase.
To estimate whether your node has enough resources to support parallel bundlers, consider two configuration values:
- Execute
UiSdlConfig.inst().infrastructurein C3 AI Console and view thebundlerMaxMemfield to see the maximum memory each bundler process may use. - Execute
UiBundlerConfig.inst().configValue('maxConcurrencyPerNode')to view the number of bundler processes allowed to run in parallel.
maxConcurrencyPerNode controls how many webpack compilation batches run simultaneously on one node. The system finds all webpack.config.js files across your packages. Each webpack.config.js exports an array of compiler configurations. These configurations are grouped into batches (controlled by developmentBatchSize or productionBatchSize), and batches from all packages compete for parallel execution slots. With 12 total configurations across all packages, developmentBatchSize: 3, and maxConcurrencyPerNode: 4: you get 4 batches of 3 configurations each running at once. Each batch processes its complete set of UI files (not split across bundlers) and requires bundlerMaxMem memory.
These values combine to determine the total memory required for UI bundling:
required memory = bundlerMaxMem * maxConcurrencyPerNodeThis required memory must fit within the free memory on the node, after accounting for memory already reserved by c3server and other services. See the example below.
Example
If a node has 32 GB total memory, and approximately 50% is reserved for c3server and other services, then about 16 GB is available for UI bundling.
With:
bundlerMaxMem: 8192(8 GB per bundler)maxConcurrencyPerNode: 2
Total required memory:
8 GB * 2 processes = 16 GBTwo bundlers may reach the node’s memory limit and cause retries or degraded performance. One bundler would be safer.
Rule of thumb
Scale concurrency linearly only when bundlerMaxMem * maxConcurrencyPerNode <= free memory on the node.
Approximate guidelines:
maxConcurrencyPerNode: 1: 8–12 GB.maxConcurrencyPerNode: 2: 16–24 GB.maxConcurrencyPerNode: 4: 32–48 GB or more.
Long webpack times, repeated retries, or memory-related failures usually indicate that the node pool's memory or CPU capacity is under-provisioned for the configured concurrency.
Overwrite the UI bundling step
See Customize the Build Pipeline in C3 AI Release Management for instructions on how to override and re-author a step in C3 AI Release Management.
In the default pipeline, UI bundling step uses a default spec. To override it, create a generateUiBundles.js file at the jarvisFilePath and define the new hardware configuration in the custom lambda.
Below is an example of how the generateUiBundles step can be overwritten:
data = {
name: "generateUiBundles",
value: (step) => {
var idSuffix = 'highConcurrencyBundling';
var shouldCreateNewBundlingStep = !step.id.includes(idSuffix);
if (shouldCreateNewBundlingStep) {
var startEnvSpec = JSON.stringify({
// example of an optimized hardware profile for ui bundling
{
"singleNode": true,
"nodePoolConfig": [
{
"name": "singlenode",
"roles": [ "*" ],
"hardwareProfile": {
"cpu": 16,
"gpu": 0,
"diskGb": 100,
"memoryMb": 64000,
"hardwareAvailability": "balanced"
},
"autoScale": {
"enabled": false
},
"sharedStorage": {
"enabled": false
},
"jvm": {
"jvmMaxMemoryPct": 0.25
},
"targetNodeCount": 1,
"minNodeCount": 1,
"maxNodeCount": 1,
"queues": {
"maxCpu": 0,
"maxFullGcCount": 0,
"maxFullGcTimeMillis": 0,
"minFreeDatastoreDiskSpacePct": 0,
"maxDatastoreFragmentationPct": 0,
"maxDatastoreQueuedTaskCount": 0,
"maxConcurrentComputes": 6
}
}
]
}
});
// Resubmit the modified step with a unique id and make sure to have it
// block the same steps as the original step (via inheritEdges "true")
Jarvis.addSteps([step.withStartEnvSpec(startEnvSpec).withId(step.id + '-' + idSuffix)], true);
return Jarvis.Step.Result.make({ step: step, status: Jarvis.Step.Status.SUCCESS });
} else {
// This is already a modified step, call the original logic
return JarvisExecutor.Helper.generateUiBundles(step);
}
}
}
The platform terminates the original generateUiBundles step and starts a new one with the specified hardware profile.
When you create a new generateUiBundles step, update the step ID with a specific marker so the custom lambda can tell whether it's running the original or modified version. Appending an idSuffix helps avoid ambiguity, and without it, you could enter an infinite loop.
Create the config file to modify UI bundling configs
See Create and View Build Configurations for C3 AI Release Management to learn how to define build configs.
In general, increase maxConcurrencyPerNode only when the hardware profile provides enough CPU and memory to support additional parallel bundlers. If concurrency is increased without sufficient resources, the bundling step may slow down instead of speeding up.
In config.js, define new UI bundler and UI SDL configs for the bundling step:
data = {
configValues: {
// Modifying `UiBundlerConfig` for dev bundling
devUiBundlerConfig: { maxConcurrencyPerNode: 4 },
// Modifying `UiBundlerConfig` for prod bundling
prodUiBundlerConfig: { maxConcurrencyPerNode: 4, developmentBatchSize: 0 },
// Modifying `UiSdlConfig` for dev bundling
devUiSdlConfig: {
infrastructure: {
bundlerMaxMem: 12288
}
},
// Modifying `UiSdlConfig` for prod bundling
prodUiSdlConfig: {
infrastructure: {
bundlerMaxMem: 12288
}
},
}
}
DEV bundling produces artifacts needed for testing changes in feature branches, while PROD bundling produces deployable assets and typically requires more memory and time. Many teams configure different hardware or concurrency settings for DEV and PROD to balance build speed with reliability.
The generateUiBundles step reads these bundling config values and uses them to start bundling jobs.
If you increase maxConcurrencyPerNode in UiBundlerConfig, also increase maxConcurrentCompute in startEnvSpec field App.NodePool.Config.queues.maxConcurrentComputes to match or exceed it. The hardware profile config sets the upper limit for concurrent compute using the maxConcurrentCompute field.
In most environments, the configuration changes that provide the largest reductions in bundling time are: increasing memory (memoryMb), ensuring the bundling step runs on a single node (singleNode: true), and preventing the node from being evicted (doNotEvict: true). These settings often reduce bundling retries and make build times more predictable.