Power Platform QA Solution
Power Platform QA
Home
Solutions
Reports
Settings
User Guide
Back to Analyzers
Configure Analyzer —
Consecutive I/O Operations
CUSTOM
Metadata
ID
consecutive-io-operations
Result Key
consecutiveIO
File
consecutive-io-operations.js
Built-in
No
Analyzer Classification
Category
— This determines the type of Analyzer you are building. Categories can be set up under settings.
Uncategorized
Performance
Data Operations
Variables & Scoping
Maintenance
Test sets
— Include this analyzer in predefined runs. Select one or more test sets this analyzer should belong to.
Dataverse test set
Some tests
Description
Detects consecutive network I/O operations that could be wrapped in Concurrent() for better performance
Prompt
Provider
Anthropic (Claude) — no key
OpenAI (GPT) — no key
Google (Gemini) — no key
Re-run AI builder
Inspect every event-driven property that contains a chain of Power Fx statements (such as OnStart, OnVisible, OnSelect, OnChange). Split the chain into top-level statements, and for each statement decide whether it performs network input/output — this is true when the statement invokes Collect, ClearCollect, Patch, Refresh, or any connector action. When two or more consecutive input/output statements at the same nesting level have no data dependency between them, raise a finding recommending they be wrapped in a Concurrent block. To test independence, build a write-set for each statement (Set writes a variable name; ClearCollect writes a collection name) and a read-set (identifiers referenced in the arguments), and confirm that a later statement does not read something a prior statement wrote. Skip groups already wrapped in Concurrent. Use Medium severity.
Source Code
consecutive-io-operations.js ·
Input data reference ↗
Checking…
Save JavaScript
export default { name: "Concurrent Network Operations", description: "Detects consecutive independent network I/O statements that could be wrapped in Concurrent() for better performance", resultKey: "concurrentNetworkOps", resultSchema: { keys: [ { key: "name", label: "Property", suggestedFormat: "name-copy" }, { key: "type", label: "Type", suggestedFormat: "badge-info" }, { key: "message", label: "Issue", suggestedFormat: "text" }, { key: "locations", label: "Locations", suggestedFormat: "locations" }, { key: "confidence", label: "Confidence", suggestedFormat: "badge-confidence" } ] }, analyze(controlTree, refGraph, extraction) { const results = []; const eventProperties = new Set([ 'OnStart', 'OnVisible', 'OnHidden', 'OnSelect', 'OnChange', 'OnCheck', 'OnUncheck', 'OnSuccess', 'OnFailure', 'OnSave', 'OnEdit', 'OnNew', 'OnCancel', 'OnReset', 'OnTimer', 'OnTimerStart', 'OnTimerEnd', 'OnLoad', 'OnRefresh' ]); const networkFunctions = new Set([ 'Collect', 'ClearCollect', 'Patch', 'Refresh', 'Remove', 'RemoveIf', 'UpdateIf' ]); // Check all nodes for event properties for (const node of controlTree.allNodes) { if (!node.formulas) continue; for (const [propName, formula] of node.formulas) { if (!eventProperties.has(propName) || !formula || formula.trim().length === 0) { continue; } const concurrentOpportunities = findConcurrentOpportunities(formula, propName, node); results.push(...concurrentOpportunities); } } return results; function findConcurrentOpportunities(formula, propName, node) { const opportunities = []; // Split formula into top-level statements (simplified approach) const statements = splitIntoStatements(formula); if (statements.length < 2) return opportunities; // Find consecutive network I/O statements let consecutiveNetworkOps = []; let startIndex = -1; for (let i = 0; i < statements.length; i++) { const stmt = statements[i]; const isNetworkOp = isNetworkOperation(stmt); const isAlreadyConcurrent = isWrappedInConcurrent(stmt); if (isNetworkOp && !isAlreadyConcurrent) { if (consecutiveNetworkOps.length === 0) { startIndex = i; } consecutiveNetworkOps.push({ statement: stmt, index: i }); } else { // End of consecutive network ops - check if we can suggest Concurrent if (consecutiveNetworkOps.length >= 2) { const canBeConcurrent = checkIndependence(consecutiveNetworkOps); if (canBeConcurrent) { opportunities.push({ name: `${node.name}.${propName}`, type: "concurrent-opportunity", message: `${consecutiveNetworkOps.length} consecutive independent network operations could be wrapped in Concurrent() for better performance`, locations: [{ control: node.name, property: propName, file: node.filePath || null, snippet: consecutiveNetworkOps.map(op => op.statement.trim()).join('; ') }], confidence: 'medium' }); } } consecutiveNetworkOps = []; startIndex = -1; } } // Check final group if (consecutiveNetworkOps.length >= 2) { const canBeConcurrent = checkIndependence(consecutiveNetworkOps); if (canBeConcurrent) { opportunities.push({ name: `${node.name}.${propName}`, type: "concurrent-opportunity", message: `${consecutiveNetworkOps.length} consecutive independent network operations could be wrapped in Concurrent() for better performance`, locations: [{ control: node.name, property: propName, file: node.filePath || null, snippet: consecutiveNetworkOps.map(op => op.statement.trim()).join('; ') }], confidence: 'medium' }); } } return opportunities; } function splitIntoStatements(formula) { // Simple statement splitting - look for semicolons not inside parentheses/quotes const statements = []; let current = ''; let parenDepth = 0; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < formula.length; i++) { const char = formula[i]; const prevChar = i > 0 ? formula[i - 1] : ''; if (!inQuotes && (char === '"' || char === "'")) { inQuotes = true; quoteChar = char; } else if (inQuotes && char === quoteChar && prevChar !== '\\') { inQuotes = false; quoteChar = ''; } else if (!inQuotes) { if (char === '(') parenDepth++; else if (char === ')') parenDepth--; else if (char === ';' && parenDepth === 0) { if (current.trim()) statements.push(current.trim()); current = ''; continue; } } current += char; } if (current.trim()) statements.push(current.trim()); return statements; } function isNetworkOperation(statement) { const trimmed = statement.trim(); // Check for direct network functions for (const func of networkFunctions) { const pattern = new RegExp(`\\b${func}\\s*\\(`, 'i'); if (pattern.test(trimmed)) return true; } // Check for connector actions (simplified - look for dotted calls) // This is a heuristic: Office365.SendEmail(), SharePoint.CreateItem(), etc. const connectorPattern = /\b[A-Z][a-zA-Z0-9]*\.[A-Z][a-zA-Z0-9]*\s*\(/; if (connectorPattern.test(trimmed)) return true; return false; } function isWrappedInConcurrent(statement) { const trimmed = statement.trim(); return /^\s*Concurrent\s*\(/i.test(trimmed); } function checkIndependence(networkOps) { // Build write and read sets for each statement const stmtAnalysis = networkOps.map(op => ({ statement: op.statement, writes: extractWrites(op.statement), reads: extractReads(op.statement) })); // Check if any later statement reads what an earlier one writes for (let i = 0; i < stmtAnalysis.length; i++) { for (let j = i + 1; j < stmtAnalysis.length; j++) { const earlier = stmtAnalysis[i]; const later = stmtAnalysis[j]; // Check if later reads what earlier writes for (const write of earlier.writes) { if (later.reads.has(write)) { return false; // Dependency found } } } } return true; // All statements are independent } function extractWrites(statement) { const writes = new Set(); // Set function writes variables const setMatch = statement.match(/\bSet\s*\(\s*([a-zA-Z_][a-zA-Z0-9_]*)/gi); if (setMatch) { for (const match of setMatch) { const varName = match.replace(/^Set\s*\(\s*/i, '').split(/[,\s)]/)[0]; if (varName) writes.add(varName); } } // ClearCollect writes collections const clearCollectMatch = statement.match(/\bClearCollect\s*\(\s*([a-zA-Z_][a-zA-Z0-9_]*)/gi); if (clearCollectMatch) { for (const match of clearCollectMatch) { const colName = match.replace(/^ClearCollect\s*\(\s*/i, '').split(/[,\s)]/)[0]; if (colName) writes.add(colName); } } // Collect also writes to collections (appends) const collectMatch = statement.match(/\bCollect\s*\(\s*([a-zA-Z_][a-zA-Z0-9_]*)/gi); if (collectMatch) { for (const match of collectMatch) { const colName = match.replace(/^Collect\s*\(\s*/i, '').split(/[,\s)]/)[0]; if (colName) writes.add(colName); } } return writes; } function extractReads(statement) { const reads = new Set(); // Extract identifiers (simplified approach) const identifierPattern = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g; const matches = statement.match(identifierPattern); if (matches) { for (const match of matches) { // Filter out Power Fx keywords and functions if (!isPowerFxKeyword(match)) { reads.add(match); } } } return reads; } function isPowerFxKeyword(word) { const keywords = new Set([ 'true', 'false', 'null', 'if', 'then', 'else', 'switch', 'with', 'as', 'Set', 'Collect', 'ClearCollect', 'Patch', 'Refresh', 'Navigate', 'Back', 'Select', 'Reset', 'UpdateContext', 'Concurrent', 'Remove', 'RemoveIf', 'UpdateIf', 'First', 'Last', 'ThisItem', 'ThisRecord', 'Parent', 'Self' ]); return keywords.has(word); } } };
Report Output Columns
Columns shown in the generated report. Toggle off to hide; pick a declared key or use "Custom…" for dot-paths like
locations.0.file
.
+ Add column
Enabled
Key
Label
Format
Width
name
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
type
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
message
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
locations
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
confidence
Plain text
Small text
Muted grey text
Code snippet
Number (right-aligned)
Percentage (%)
Truncate at 80 chars
Truncate at 120 chars
Info pill (blue)
Confidence pill (red/yellow/grey)
Dead-code type pill
Bold name + copy button
Locations list
×
Cancel
Test analyzer
Save Columns
Running analyzer against solution…
This can take up to a minute for large solutions.
Test Consecutive I/O Operations
×
Runs this analyzer in isolation against the chosen solution. Nothing is saved — the result is shown here only.
Use an existing solution
No solutions uploaded yet
Upload a solution file
Cancel
Run test
Test results
×