@netoalmanca/advpl-sensei 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/changelog-generator.md +63 -0
- package/agents/code-generator.md +215 -0
- package/agents/code-reviewer.md +145 -0
- package/agents/debugger.md +83 -0
- package/agents/doc-generator.md +67 -0
- package/agents/docs-reference.md +86 -0
- package/agents/migrator.md +84 -0
- package/agents/process-consultant.md +97 -0
- package/agents/refactorer.md +75 -0
- package/agents/sx-configurator.md +67 -0
- package/commands/changelog.md +66 -0
- package/commands/diagnose.md +67 -0
- package/commands/docs.md +81 -0
- package/commands/document.md +67 -0
- package/commands/explain.md +60 -0
- package/commands/generate.md +111 -0
- package/commands/migrate.md +81 -0
- package/commands/process.md +111 -0
- package/commands/refactor.md +65 -0
- package/commands/review.md +60 -0
- package/commands/sxgen.md +98 -0
- package/commands/test.md +103 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
- package/skills/advpl-code-generation/SKILL.md +163 -0
- package/skills/advpl-code-generation/patterns-fwformbrowse.md +485 -0
- package/skills/advpl-code-generation/patterns-jobs.md +519 -0
- package/skills/advpl-code-generation/patterns-mvc.md +765 -0
- package/skills/advpl-code-generation/patterns-pontos-entrada.md +708 -0
- package/skills/advpl-code-generation/patterns-rest.md +974 -0
- package/skills/advpl-code-generation/patterns-soap.md +639 -0
- package/skills/advpl-code-generation/patterns-treport.md +481 -0
- package/skills/advpl-code-generation/patterns-workflow.md +779 -0
- package/skills/advpl-code-generation/templates-classes.md +1096 -0
- package/skills/advpl-code-review/SKILL.md +72 -0
- package/skills/advpl-code-review/rules-best-practices.md +444 -0
- package/skills/advpl-code-review/rules-modernization.md +290 -0
- package/skills/advpl-code-review/rules-performance.md +333 -0
- package/skills/advpl-code-review/rules-security.md +302 -0
- package/skills/advpl-debugging/SKILL.md +265 -0
- package/skills/advpl-debugging/common-errors.md +1124 -0
- package/skills/advpl-debugging/performance-tips.md +768 -0
- package/skills/advpl-refactoring/SKILL.md +139 -0
- package/skills/advpl-to-tlpp-migration/SKILL.md +293 -0
- package/skills/advpl-to-tlpp-migration/migration-checklist.md +122 -0
- package/skills/advpl-to-tlpp-migration/migration-rules.md +265 -0
- package/skills/changelog-patterns/SKILL.md +99 -0
- package/skills/code-explanation/SKILL.md +66 -0
- package/skills/documentation-patterns/SKILL.md +172 -0
- package/skills/embedded-sql/SKILL.md +379 -0
- package/skills/probat-testing/SKILL.md +226 -0
- package/skills/probat-testing/patterns-unit-tests.md +614 -0
- package/skills/protheus-business/SKILL.md +92 -0
- package/skills/protheus-business/modulo-compras.md +780 -0
- package/skills/protheus-business/modulo-contabilidade.md +874 -0
- package/skills/protheus-business/modulo-estoque.md +876 -0
- package/skills/protheus-business/modulo-faturamento.md +800 -0
- package/skills/protheus-business/modulo-financeiro.md +1015 -0
- package/skills/protheus-business/modulo-fiscal.md +749 -0
- package/skills/protheus-business/modulo-manutencao.md +848 -0
- package/skills/protheus-business/modulo-pcp.md +743 -0
- package/skills/protheus-reference/SKILL.md +119 -0
- package/skills/protheus-reference/native-functions.md +7029 -0
- package/skills/protheus-reference/rest-api-reference.md +1758 -0
- package/skills/protheus-reference/restricted-functions.md +265 -0
- package/skills/protheus-reference/sx-dictionary.md +854 -0
- package/skills/sx-configuration/SKILL.md +184 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
# ADVPL/TLPP Performance Optimization Tips
|
|
2
|
+
|
|
3
|
+
Practical techniques for improving performance in ADVPL/TLPP code on TOTVS Protheus. Each tip includes a before (slow) and after (optimized) code example.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Index Optimization
|
|
8
|
+
|
|
9
|
+
### Choosing the Right DbSetOrder
|
|
10
|
+
|
|
11
|
+
Using the wrong index forces a sequential scan instead of an indexed seek. Always match `DbSetOrder` to the key you are searching.
|
|
12
|
+
|
|
13
|
+
**Before (slow):**
|
|
14
|
+
```advpl
|
|
15
|
+
// Using index 1 (A1_FILIAL+A1_COD+A1_LOJA) but searching by name
|
|
16
|
+
DbSelectArea("SA1")
|
|
17
|
+
DbSetOrder(1)
|
|
18
|
+
DbGoTop()
|
|
19
|
+
While !Eof()
|
|
20
|
+
If Alltrim(SA1->A1_NOME) == cNomeBusca // full table scan
|
|
21
|
+
// found
|
|
22
|
+
Exit
|
|
23
|
+
EndIf
|
|
24
|
+
DbSkip()
|
|
25
|
+
EndDo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**After (optimized):**
|
|
29
|
+
```advpl
|
|
30
|
+
// Use the index that matches the search key (e.g., index 5 = A1_FILIAL+A1_NOME)
|
|
31
|
+
DbSelectArea("SA1")
|
|
32
|
+
DbSetOrder(5) // index on A1_FILIAL + A1_NOME
|
|
33
|
+
If DbSeek(xFilial("SA1") + PadR(cNomeBusca, TamSX3("A1_NOME")[1]))
|
|
34
|
+
// found via indexed seek - much faster
|
|
35
|
+
EndIf
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Understanding Index Composition
|
|
39
|
+
|
|
40
|
+
Before choosing an index, check SIX for the table:
|
|
41
|
+
|
|
42
|
+
```advpl
|
|
43
|
+
// Query SIX to see available indexes for SA1
|
|
44
|
+
DbSelectArea("SIX")
|
|
45
|
+
DbSetOrder(1) // INDICE+ORDEM
|
|
46
|
+
DbSeek("SA1")
|
|
47
|
+
While !Eof() .And. SIX->INDICE == "SA1"
|
|
48
|
+
Conout("Order: " + SIX->ORDEM + " Key: " + Alltrim(SIX->CHAVE))
|
|
49
|
+
DbSkip()
|
|
50
|
+
EndDo
|
|
51
|
+
// Output:
|
|
52
|
+
// Order: 1 Key: A1_FILIAL+A1_COD+A1_LOJA
|
|
53
|
+
// Order: 2 Key: A1_FILIAL+A1_CGC
|
|
54
|
+
// Order: 3 Key: A1_FILIAL+A1_NOME
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### When to Create Custom Indexes
|
|
58
|
+
|
|
59
|
+
Create custom indexes when your frequent queries do not match any existing index:
|
|
60
|
+
|
|
61
|
+
```advpl
|
|
62
|
+
// Creating a temporary index for a specific report
|
|
63
|
+
Local cIdxFile := CriaTrab(NIL, .F.)
|
|
64
|
+
Local cIdxKey := "D1_FILIAL+D1_FORNECE+D1_EMISSAO"
|
|
65
|
+
|
|
66
|
+
DbSelectArea("SD1")
|
|
67
|
+
IndRegua("SD1", cIdxFile, cIdxKey, , , "Creating index...")
|
|
68
|
+
DbSetIndex(cIdxFile + OrdBagExt())
|
|
69
|
+
DbSetOrder(IndexOrd())
|
|
70
|
+
|
|
71
|
+
// Use the index
|
|
72
|
+
DbGoTop()
|
|
73
|
+
While !Eof()
|
|
74
|
+
// process in order of FORNECE+EMISSAO
|
|
75
|
+
DbSkip()
|
|
76
|
+
EndDo
|
|
77
|
+
|
|
78
|
+
// Clean up temporary index
|
|
79
|
+
DbClearIndex()
|
|
80
|
+
FErase(cIdxFile + OrdBagExt())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 2. Query Optimization
|
|
86
|
+
|
|
87
|
+
### Embedded SQL vs ISAM Access
|
|
88
|
+
|
|
89
|
+
Use ISAM (`DbSeek`) for single-record lookups by key. Use embedded SQL for filtered, aggregated, or multi-table queries.
|
|
90
|
+
|
|
91
|
+
**Before (slow) - ISAM for aggregation:**
|
|
92
|
+
```advpl
|
|
93
|
+
// Summing values using ISAM loop - very slow for large tables
|
|
94
|
+
DbSelectArea("SD1")
|
|
95
|
+
DbSetOrder(1)
|
|
96
|
+
DbSeek(xFilial("SD1") + cDoc)
|
|
97
|
+
nTotal := 0
|
|
98
|
+
While !Eof() .And. SD1->D1_FILIAL == xFilial("SD1") .And. SD1->D1_DOC == cDoc
|
|
99
|
+
nTotal += SD1->D1_TOTAL
|
|
100
|
+
DbSkip()
|
|
101
|
+
EndDo
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**After (optimized) - SQL for aggregation:**
|
|
105
|
+
```advpl
|
|
106
|
+
// SQL aggregation is handled by the database engine - much faster
|
|
107
|
+
Local cQuery := ""
|
|
108
|
+
cQuery += "SELECT SUM(D1_TOTAL) AS TOTAL "
|
|
109
|
+
cQuery += "FROM " + RetSqlName("SD1") + " SD1 "
|
|
110
|
+
cQuery += "WHERE D1_FILIAL = '" + xFilial("SD1") + "' "
|
|
111
|
+
cQuery += "AND D1_DOC = '" + cDoc + "' "
|
|
112
|
+
cQuery += "AND SD1.D_E_L_E_T_ = ' ' "
|
|
113
|
+
|
|
114
|
+
TCQuery cQuery New Alias "QRY_SUM"
|
|
115
|
+
nTotal := QRY_SUM->TOTAL
|
|
116
|
+
DbSelectArea("QRY_SUM")
|
|
117
|
+
DbCloseArea()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### SELECT Optimization Tips
|
|
121
|
+
|
|
122
|
+
```advpl
|
|
123
|
+
// TIP 1: Select only needed columns (avoid SELECT *)
|
|
124
|
+
// WRONG
|
|
125
|
+
cQuery := "SELECT * FROM " + RetSqlName("SA1") + " SA1 "
|
|
126
|
+
// RIGHT
|
|
127
|
+
cQuery := "SELECT A1_COD, A1_LOJA, A1_NOME FROM " + RetSqlName("SA1") + " SA1 "
|
|
128
|
+
|
|
129
|
+
// TIP 2: Always filter by D_E_L_E_T_
|
|
130
|
+
cQuery += "WHERE SA1.D_E_L_E_T_ = ' ' "
|
|
131
|
+
|
|
132
|
+
// TIP 3: Always filter by branch (filial)
|
|
133
|
+
cQuery += "AND A1_FILIAL = '" + xFilial("SA1") + "' "
|
|
134
|
+
|
|
135
|
+
// TIP 4: Use NOLOCK hint (SQL Server) for read-only queries
|
|
136
|
+
cQuery := "SELECT A1_COD, A1_NOME FROM " + RetSqlName("SA1") + " SA1 WITH(NOLOCK) "
|
|
137
|
+
cQuery += "WHERE SA1.D_E_L_E_T_ = ' ' "
|
|
138
|
+
|
|
139
|
+
// TIP 5: Limit results when only checking existence
|
|
140
|
+
cQuery := "SELECT TOP 1 A1_COD FROM " + RetSqlName("SA1") + " SA1 "
|
|
141
|
+
cQuery += "WHERE A1_FILIAL = '" + xFilial("SA1") + "' "
|
|
142
|
+
cQuery += "AND A1_CGC = '" + cCGC + "' "
|
|
143
|
+
cQuery += "AND SA1.D_E_L_E_T_ = ' ' "
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 3. Memory Management
|
|
149
|
+
|
|
150
|
+
### Array Pre-allocation with aSize
|
|
151
|
+
|
|
152
|
+
**Before (slow):**
|
|
153
|
+
```advpl
|
|
154
|
+
// aAdd reallocates memory on every call
|
|
155
|
+
Local aResult := {}
|
|
156
|
+
Local nCount := 5000
|
|
157
|
+
|
|
158
|
+
For nI := 1 To nCount
|
|
159
|
+
aAdd(aResult, {"", 0, .F.})
|
|
160
|
+
Next
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**After (optimized):**
|
|
164
|
+
```advpl
|
|
165
|
+
// Pre-allocate the array to the known size
|
|
166
|
+
Local nCount := 5000
|
|
167
|
+
Local aResult := Array(nCount)
|
|
168
|
+
|
|
169
|
+
For nI := 1 To nCount
|
|
170
|
+
aResult[nI] := {"", 0, .F.}
|
|
171
|
+
Next
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Object Destruction with FreeObj
|
|
175
|
+
|
|
176
|
+
**Before (slow) - memory leak:**
|
|
177
|
+
```advpl
|
|
178
|
+
// Objects never freed - accumulate in memory
|
|
179
|
+
While !Eof()
|
|
180
|
+
Local oItem := ItemClass():New()
|
|
181
|
+
oItem:Load(ALIAS->CODE)
|
|
182
|
+
aAdd(aItems, oItem)
|
|
183
|
+
DbSkip()
|
|
184
|
+
EndDo
|
|
185
|
+
// oItem objects stay in memory even after aItems goes out of scope
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**After (optimized):**
|
|
189
|
+
```advpl
|
|
190
|
+
// Free objects when done
|
|
191
|
+
While !Eof()
|
|
192
|
+
Local oItem := ItemClass():New()
|
|
193
|
+
oItem:Load(ALIAS->CODE)
|
|
194
|
+
aAdd(aItems, oItem:Export()) // export data, not object
|
|
195
|
+
FreeObj(oItem) // release object memory
|
|
196
|
+
DbSkip()
|
|
197
|
+
EndDo
|
|
198
|
+
|
|
199
|
+
// If you must keep objects in array, free them at the end:
|
|
200
|
+
For nI := 1 To Len(aItems)
|
|
201
|
+
If ValType(aItems[nI]) == "O"
|
|
202
|
+
FreeObj(aItems[nI])
|
|
203
|
+
EndIf
|
|
204
|
+
Next
|
|
205
|
+
aSize(aItems, 0)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Avoiding Memory Leaks in Loops
|
|
209
|
+
|
|
210
|
+
**Before (slow):**
|
|
211
|
+
```advpl
|
|
212
|
+
// TCQuery opens alias but never closes it - each iteration leaks a workarea
|
|
213
|
+
For nI := 1 To Len(aDocs)
|
|
214
|
+
cQuery := "SELECT D1_TOTAL FROM " + RetSqlName("SD1") + " WHERE D1_DOC = '" + aDocs[nI] + "'"
|
|
215
|
+
TCQuery cQuery New Alias "QRY_TMP"
|
|
216
|
+
nTotal += QRY_TMP->D1_TOTAL
|
|
217
|
+
// Missing: DbCloseArea()
|
|
218
|
+
Next
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**After (optimized):**
|
|
222
|
+
```advpl
|
|
223
|
+
// Always close temporary aliases inside loops
|
|
224
|
+
For nI := 1 To Len(aDocs)
|
|
225
|
+
cQuery := "SELECT D1_TOTAL FROM " + RetSqlName("SD1") + " SD1 "
|
|
226
|
+
cQuery += "WHERE D1_DOC = '" + aDocs[nI] + "' "
|
|
227
|
+
cQuery += "AND SD1.D_E_L_E_T_ = ' ' "
|
|
228
|
+
TCQuery cQuery New Alias "QRY_TMP"
|
|
229
|
+
If Select("QRY_TMP") > 0
|
|
230
|
+
nTotal += QRY_TMP->D1_TOTAL
|
|
231
|
+
DbSelectArea("QRY_TMP")
|
|
232
|
+
DbCloseArea()
|
|
233
|
+
EndIf
|
|
234
|
+
Next
|
|
235
|
+
|
|
236
|
+
// Even better - use a single query with IN clause
|
|
237
|
+
cInList := ""
|
|
238
|
+
For nI := 1 To Len(aDocs)
|
|
239
|
+
cInList += IIf(!Empty(cInList), ",", "") + "'" + aDocs[nI] + "'"
|
|
240
|
+
Next
|
|
241
|
+
cQuery := "SELECT D1_DOC, D1_TOTAL FROM " + RetSqlName("SD1") + " SD1 "
|
|
242
|
+
cQuery += "WHERE D1_DOC IN (" + cInList + ") "
|
|
243
|
+
cQuery += "AND SD1.D_E_L_E_T_ = ' ' "
|
|
244
|
+
TCQuery cQuery New Alias "QRY_BATCH"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 4. Network Optimization
|
|
250
|
+
|
|
251
|
+
### Batch vs Individual Operations
|
|
252
|
+
|
|
253
|
+
**Before (slow):**
|
|
254
|
+
```advpl
|
|
255
|
+
// One HTTP call per item - N round trips
|
|
256
|
+
For nI := 1 To Len(aOrders)
|
|
257
|
+
oRest := FWRest():New(cBaseUrl)
|
|
258
|
+
oRest:SetPostParams(aOrders[nI]:ToJson())
|
|
259
|
+
oRest:Post("/api/orders")
|
|
260
|
+
FreeObj(oRest)
|
|
261
|
+
Next
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**After (optimized):**
|
|
265
|
+
```advpl
|
|
266
|
+
// Single HTTP call with batch payload - 1 round trip
|
|
267
|
+
Local oJson := JsonObject():New()
|
|
268
|
+
oJson:SetJsonObject("orders", aOrders)
|
|
269
|
+
|
|
270
|
+
oRest := FWRest():New(cBaseUrl)
|
|
271
|
+
oRest:SetPostParams(oJson:ToJson())
|
|
272
|
+
oRest:Post("/api/orders/batch")
|
|
273
|
+
|
|
274
|
+
FreeObj(oJson)
|
|
275
|
+
FreeObj(oRest)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Payload Size Reduction
|
|
279
|
+
|
|
280
|
+
**Before (slow):**
|
|
281
|
+
```advpl
|
|
282
|
+
// Sending entire record with all fields
|
|
283
|
+
oJson := JsonObject():New()
|
|
284
|
+
While !Eof()
|
|
285
|
+
oItem := JsonObject():New()
|
|
286
|
+
// Adding all 50+ fields when only 5 are needed
|
|
287
|
+
For nI := 1 To FCount()
|
|
288
|
+
oItem:SetJsonText(FieldName(nI), FieldGet(nI))
|
|
289
|
+
Next
|
|
290
|
+
oJson:Append(oItem)
|
|
291
|
+
DbSkip()
|
|
292
|
+
EndDo
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**After (optimized):**
|
|
296
|
+
```advpl
|
|
297
|
+
// Send only the required fields
|
|
298
|
+
oJson := JsonObject():New()
|
|
299
|
+
While !Eof()
|
|
300
|
+
oItem := JsonObject():New()
|
|
301
|
+
oItem:SetJsonText("code", Alltrim(SA1->A1_COD))
|
|
302
|
+
oItem:SetJsonText("name", Alltrim(SA1->A1_NOME))
|
|
303
|
+
oItem:SetJsonText("cgc", Alltrim(SA1->A1_CGC))
|
|
304
|
+
oJson:Append(oItem)
|
|
305
|
+
FreeObj(oItem)
|
|
306
|
+
DbSkip()
|
|
307
|
+
EndDo
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 5. UI Performance
|
|
313
|
+
|
|
314
|
+
### Lazy Loading in Browses
|
|
315
|
+
|
|
316
|
+
**Before (slow):**
|
|
317
|
+
```advpl
|
|
318
|
+
// Loading all records into array before displaying
|
|
319
|
+
Local aData := {}
|
|
320
|
+
DbSelectArea("SC5")
|
|
321
|
+
DbGoTop()
|
|
322
|
+
While !Eof()
|
|
323
|
+
aAdd(aData, {SC5->C5_NUM, SC5->C5_EMISSAO, SC5->C5_CLIENTE})
|
|
324
|
+
DbSkip()
|
|
325
|
+
EndDo
|
|
326
|
+
// Display array - slow if SC5 has millions of records
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**After (optimized):**
|
|
330
|
+
```advpl
|
|
331
|
+
// Use MsBrowse with database-backed browsing (no pre-load)
|
|
332
|
+
DbSelectArea("SC5")
|
|
333
|
+
DbSetOrder(1)
|
|
334
|
+
DbGoTop()
|
|
335
|
+
|
|
336
|
+
oBrowse := MsBrowse():New(01, 01, 20, 75, , , , , , , , "SC5")
|
|
337
|
+
oBrowse:AddColumn("C5_NUM", , "Pedido", , 15)
|
|
338
|
+
oBrowse:AddColumn("C5_EMISSAO", , "Emissao", , 10)
|
|
339
|
+
oBrowse:AddColumn("C5_CLIENTE", , "Cliente", , 20)
|
|
340
|
+
// Records are loaded on demand as user scrolls
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Reducing UI Refreshes
|
|
344
|
+
|
|
345
|
+
**Before (slow):**
|
|
346
|
+
```advpl
|
|
347
|
+
// Refreshing dialog on every iteration
|
|
348
|
+
For nI := 1 To Len(aItems)
|
|
349
|
+
ProcessItem(aItems[nI])
|
|
350
|
+
oDialog:Refresh() // forces UI repaint on every item
|
|
351
|
+
ProcRegua(nI)
|
|
352
|
+
Next
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**After (optimized):**
|
|
356
|
+
```advpl
|
|
357
|
+
// Refresh only at intervals
|
|
358
|
+
Local nRefreshInterval := Max(1, Int(Len(aItems) / 20)) // ~20 refreshes total
|
|
359
|
+
For nI := 1 To Len(aItems)
|
|
360
|
+
ProcessItem(aItems[nI])
|
|
361
|
+
If nI % nRefreshInterval == 0
|
|
362
|
+
ProcRegua(nI)
|
|
363
|
+
EndIf
|
|
364
|
+
Next
|
|
365
|
+
oDialog:Refresh() // single refresh at end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 6. Temporary Tables (TRB)
|
|
371
|
+
|
|
372
|
+
### When to Use Temp Tables
|
|
373
|
+
|
|
374
|
+
Use temporary tables (TRB) for intermediate processing when you need indexed access to computed data.
|
|
375
|
+
|
|
376
|
+
**Before (slow):**
|
|
377
|
+
```advpl
|
|
378
|
+
// Processing with nested loops - O(n*m) complexity
|
|
379
|
+
For nI := 1 To Len(aPedidos)
|
|
380
|
+
For nJ := 1 To Len(aItens)
|
|
381
|
+
If aItens[nJ][1] == aPedidos[nI][1]
|
|
382
|
+
// match found
|
|
383
|
+
EndIf
|
|
384
|
+
Next
|
|
385
|
+
Next
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**After (optimized):**
|
|
389
|
+
```advpl
|
|
390
|
+
// Create TRB for indexed access
|
|
391
|
+
Local cTrb := CriaTrab(NIL, .F.)
|
|
392
|
+
|
|
393
|
+
DbCreate(cTrb, {;
|
|
394
|
+
{"PEDIDO", "C", 15, 0},;
|
|
395
|
+
{"ITEM", "C", 6, 0},;
|
|
396
|
+
{"TOTAL", "N", 14, 2};
|
|
397
|
+
}, "DBFCDXADS")
|
|
398
|
+
|
|
399
|
+
DbUseArea(.T., "DBFCDXADS", cTrb, "TMP_ITENS", .F., .F.)
|
|
400
|
+
IndRegua("TMP_ITENS", cTrb, "PEDIDO+ITEM")
|
|
401
|
+
|
|
402
|
+
// Populate TRB
|
|
403
|
+
For nI := 1 To Len(aItens)
|
|
404
|
+
RecLock("TMP_ITENS", .T.)
|
|
405
|
+
TMP_ITENS->PEDIDO := aItens[nI][1]
|
|
406
|
+
TMP_ITENS->ITEM := aItens[nI][2]
|
|
407
|
+
TMP_ITENS->TOTAL := aItens[nI][3]
|
|
408
|
+
MsUnlock()
|
|
409
|
+
Next
|
|
410
|
+
|
|
411
|
+
// Now use indexed seek instead of nested loops
|
|
412
|
+
DbSelectArea("TMP_ITENS")
|
|
413
|
+
DbSetOrder(1)
|
|
414
|
+
For nI := 1 To Len(aPedidos)
|
|
415
|
+
If DbSeek(aPedidos[nI][1])
|
|
416
|
+
While !Eof() .And. TMP_ITENS->PEDIDO == aPedidos[nI][1]
|
|
417
|
+
// process matching items
|
|
418
|
+
DbSkip()
|
|
419
|
+
EndDo
|
|
420
|
+
EndIf
|
|
421
|
+
Next
|
|
422
|
+
|
|
423
|
+
// Cleanup
|
|
424
|
+
DbSelectArea("TMP_ITENS")
|
|
425
|
+
DbCloseArea()
|
|
426
|
+
FErase(cTrb + ".dbf")
|
|
427
|
+
FErase(cTrb + OrdBagExt())
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## 7. Cache Patterns
|
|
433
|
+
|
|
434
|
+
### Caching GetMV Results
|
|
435
|
+
|
|
436
|
+
**Before (slow):**
|
|
437
|
+
```advpl
|
|
438
|
+
// Calling GetMV on every loop iteration - hits SX6 table each time
|
|
439
|
+
While !Eof()
|
|
440
|
+
If SA1->A1_TIPO == GetMV("MV_TIPOCLI") // DB lookup on every iteration
|
|
441
|
+
// process
|
|
442
|
+
EndIf
|
|
443
|
+
DbSkip()
|
|
444
|
+
EndDo
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**After (optimized):**
|
|
448
|
+
```advpl
|
|
449
|
+
// Cache the parameter value before the loop
|
|
450
|
+
Local cTipoCli := SuperGetMV("MV_TIPOCLI", .F., "F") // with default value
|
|
451
|
+
|
|
452
|
+
While !Eof()
|
|
453
|
+
If SA1->A1_TIPO == cTipoCli // uses cached value
|
|
454
|
+
// process
|
|
455
|
+
EndIf
|
|
456
|
+
DbSkip()
|
|
457
|
+
EndDo
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Caching Repeated Database Lookups
|
|
461
|
+
|
|
462
|
+
**Before (slow):**
|
|
463
|
+
```advpl
|
|
464
|
+
// Looking up client name for every invoice line
|
|
465
|
+
DbSelectArea("SD2")
|
|
466
|
+
DbSetOrder(1)
|
|
467
|
+
DbGoTop()
|
|
468
|
+
While !Eof()
|
|
469
|
+
// Opens SA1 seek on every line - even if same client repeats
|
|
470
|
+
DbSelectArea("SA1")
|
|
471
|
+
DbSetOrder(1)
|
|
472
|
+
If DbSeek(xFilial("SA1") + SD2->D2_CLIENTE + SD2->D2_LOJA)
|
|
473
|
+
cNome := SA1->A1_NOME
|
|
474
|
+
EndIf
|
|
475
|
+
DbSelectArea("SD2")
|
|
476
|
+
DbSkip()
|
|
477
|
+
EndDo
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**After (optimized):**
|
|
481
|
+
```advpl
|
|
482
|
+
// Cache lookups in a hash-like array
|
|
483
|
+
Local aCache := {}
|
|
484
|
+
Local cKey := ""
|
|
485
|
+
Local cNome := ""
|
|
486
|
+
Local nPos := 0
|
|
487
|
+
|
|
488
|
+
DbSelectArea("SD2")
|
|
489
|
+
DbSetOrder(1)
|
|
490
|
+
DbGoTop()
|
|
491
|
+
While !Eof()
|
|
492
|
+
cKey := SD2->D2_CLIENTE + SD2->D2_LOJA
|
|
493
|
+
|
|
494
|
+
nPos := aScan(aCache, {|x| x[1] == cKey})
|
|
495
|
+
If nPos > 0
|
|
496
|
+
cNome := aCache[nPos][2] // from cache
|
|
497
|
+
Else
|
|
498
|
+
DbSelectArea("SA1")
|
|
499
|
+
DbSetOrder(1)
|
|
500
|
+
If DbSeek(xFilial("SA1") + cKey)
|
|
501
|
+
cNome := SA1->A1_NOME
|
|
502
|
+
Else
|
|
503
|
+
cNome := ""
|
|
504
|
+
EndIf
|
|
505
|
+
aAdd(aCache, {cKey, cNome})
|
|
506
|
+
EndIf
|
|
507
|
+
|
|
508
|
+
// use cNome
|
|
509
|
+
DbSelectArea("SD2")
|
|
510
|
+
DbSkip()
|
|
511
|
+
EndDo
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Static Variables for Caching
|
|
515
|
+
|
|
516
|
+
```advpl
|
|
517
|
+
// Use Static to cache data that does not change during execution
|
|
518
|
+
Static cCompanyName := NIL
|
|
519
|
+
|
|
520
|
+
Static Function GetCompanyName()
|
|
521
|
+
If cCompanyName == NIL
|
|
522
|
+
// Only queries once, caches for entire session
|
|
523
|
+
cCompanyName := Alltrim(GetMV("MV_NOMEMP"))
|
|
524
|
+
EndIf
|
|
525
|
+
Return cCompanyName
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## 8. Transaction Scope
|
|
531
|
+
|
|
532
|
+
### Keeping Transactions Short
|
|
533
|
+
|
|
534
|
+
**Before (slow):**
|
|
535
|
+
```advpl
|
|
536
|
+
// Transaction wraps everything including validation and logging
|
|
537
|
+
Begin Transaction
|
|
538
|
+
// Validation queries (read-only - do not need transaction)
|
|
539
|
+
DbSelectArea("SA1")
|
|
540
|
+
DbSetOrder(1)
|
|
541
|
+
If !DbSeek(xFilial("SA1") + cCodCli)
|
|
542
|
+
DisarmTransaction()
|
|
543
|
+
EndIf
|
|
544
|
+
|
|
545
|
+
// Heavy logging
|
|
546
|
+
FWLogMsg("INFO", , , , , , "Starting process...")
|
|
547
|
+
|
|
548
|
+
// The actual write (this is what needs the transaction)
|
|
549
|
+
RecLock("SC5", .T.)
|
|
550
|
+
SC5->C5_NUM := cNumPed
|
|
551
|
+
SC5->C5_CLIENTE := cCodCli
|
|
552
|
+
MsUnlock()
|
|
553
|
+
|
|
554
|
+
// More logging
|
|
555
|
+
FWLogMsg("INFO", , , , , , "Process complete")
|
|
556
|
+
End Transaction
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**After (optimized):**
|
|
560
|
+
```advpl
|
|
561
|
+
// Validate first, outside the transaction
|
|
562
|
+
DbSelectArea("SA1")
|
|
563
|
+
DbSetOrder(1)
|
|
564
|
+
If !DbSeek(xFilial("SA1") + cCodCli)
|
|
565
|
+
Conout("Client not found: " + cCodCli)
|
|
566
|
+
Return .F.
|
|
567
|
+
EndIf
|
|
568
|
+
|
|
569
|
+
FWLogMsg("INFO", , , , , , "Starting process...")
|
|
570
|
+
|
|
571
|
+
// Transaction wraps only the writes - minimal scope
|
|
572
|
+
Begin Transaction
|
|
573
|
+
RecLock("SC5", .T.)
|
|
574
|
+
SC5->C5_NUM := cNumPed
|
|
575
|
+
SC5->C5_CLIENTE := cCodCli
|
|
576
|
+
MsUnlock()
|
|
577
|
+
End Transaction
|
|
578
|
+
|
|
579
|
+
FWLogMsg("INFO", , , , , , "Process complete")
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### What to Include/Exclude from Transactions
|
|
583
|
+
|
|
584
|
+
| Include in Transaction | Exclude from Transaction |
|
|
585
|
+
|----------------------|------------------------|
|
|
586
|
+
| RecLock / MsUnlock (writes) | Read-only queries (DbSeek for validation) |
|
|
587
|
+
| Multiple related writes that must be atomic | Logging (Conout, FWLogMsg) |
|
|
588
|
+
| TCSqlExec for DML statements | User interaction (MsgInfo, MsgYesNo) |
|
|
589
|
+
| | Network calls (REST APIs) |
|
|
590
|
+
| | File I/O operations |
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## 9. String Concatenation
|
|
595
|
+
|
|
596
|
+
### Using Arrays Instead of Repeated Concatenation
|
|
597
|
+
|
|
598
|
+
**Before (slow):**
|
|
599
|
+
```advpl
|
|
600
|
+
// String concatenation in loop - each += creates a new string copy
|
|
601
|
+
Local cReport := ""
|
|
602
|
+
Local nLines := 10000
|
|
603
|
+
|
|
604
|
+
DbSelectArea("SD1")
|
|
605
|
+
DbGoTop()
|
|
606
|
+
While !Eof()
|
|
607
|
+
cReport += SD1->D1_DOC + " | "
|
|
608
|
+
cReport += SD1->D1_SERIE + " | "
|
|
609
|
+
cReport += cValToChar(SD1->D1_TOTAL) + CRLF
|
|
610
|
+
DbSkip()
|
|
611
|
+
EndDo
|
|
612
|
+
// Performance degrades exponentially as cReport grows
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
**After (optimized):**
|
|
616
|
+
```advpl
|
|
617
|
+
// Build with array, join at the end
|
|
618
|
+
Local aReport := {}
|
|
619
|
+
|
|
620
|
+
DbSelectArea("SD1")
|
|
621
|
+
DbGoTop()
|
|
622
|
+
While !Eof()
|
|
623
|
+
aAdd(aReport, SD1->D1_DOC + " | " + ;
|
|
624
|
+
SD1->D1_SERIE + " | " + ;
|
|
625
|
+
cValToChar(SD1->D1_TOTAL))
|
|
626
|
+
DbSkip()
|
|
627
|
+
EndDo
|
|
628
|
+
|
|
629
|
+
// Single join at the end
|
|
630
|
+
Local cReport := ""
|
|
631
|
+
For nI := 1 To Len(aReport)
|
|
632
|
+
cReport += aReport[nI] + CRLF
|
|
633
|
+
Next
|
|
634
|
+
|
|
635
|
+
// Or for file output, write line by line instead of building full string:
|
|
636
|
+
Local nHandle := FCreate(cFilePath)
|
|
637
|
+
For nI := 1 To Len(aReport)
|
|
638
|
+
FWrite(nHandle, aReport[nI] + CRLF)
|
|
639
|
+
Next
|
|
640
|
+
FClose(nHandle)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## 10. Loop Optimization
|
|
646
|
+
|
|
647
|
+
### Moving Invariants Outside Loops
|
|
648
|
+
|
|
649
|
+
**Before (slow):**
|
|
650
|
+
```advpl
|
|
651
|
+
// xFilial, TamSX3, and GetMV called on every iteration - wasteful
|
|
652
|
+
DbSelectArea("SA1")
|
|
653
|
+
DbGoTop()
|
|
654
|
+
While !Eof()
|
|
655
|
+
If SA1->A1_FILIAL == xFilial("SA1") // recalculates every time
|
|
656
|
+
If SA1->A1_TIPO == GetMV("MV_TIPOCLI") // DB lookup every time
|
|
657
|
+
cNome := PadR(Alltrim(SA1->A1_NOME), TamSX3("A1_NOME")[1]) // TamSX3 every time
|
|
658
|
+
EndIf
|
|
659
|
+
EndIf
|
|
660
|
+
DbSkip()
|
|
661
|
+
EndDo
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
**After (optimized):**
|
|
665
|
+
```advpl
|
|
666
|
+
// Calculate invariants once before the loop
|
|
667
|
+
Local cFilSA1 := xFilial("SA1")
|
|
668
|
+
Local cTipoCli := SuperGetMV("MV_TIPOCLI", .F., "F")
|
|
669
|
+
Local nTamNome := TamSX3("A1_NOME")[1]
|
|
670
|
+
|
|
671
|
+
DbSelectArea("SA1")
|
|
672
|
+
DbGoTop()
|
|
673
|
+
While !Eof()
|
|
674
|
+
If SA1->A1_FILIAL == cFilSA1
|
|
675
|
+
If SA1->A1_TIPO == cTipoCli
|
|
676
|
+
cNome := PadR(Alltrim(SA1->A1_NOME), nTamNome)
|
|
677
|
+
EndIf
|
|
678
|
+
EndIf
|
|
679
|
+
DbSkip()
|
|
680
|
+
EndDo
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Proper Loop Termination
|
|
684
|
+
|
|
685
|
+
**Before (slow):**
|
|
686
|
+
```advpl
|
|
687
|
+
// Scans entire table even after passing the relevant records
|
|
688
|
+
DbSelectArea("SD1")
|
|
689
|
+
DbSetOrder(1) // D1_FILIAL+D1_DOC+D1_SERIE+D1_ITEM
|
|
690
|
+
DbSeek(xFilial("SD1") + cDoc + cSerie)
|
|
691
|
+
While !Eof()
|
|
692
|
+
If SD1->D1_DOC == cDoc .And. SD1->D1_SERIE == cSerie
|
|
693
|
+
nTotal += SD1->D1_TOTAL
|
|
694
|
+
EndIf // continues to end of table even when D1_DOC changes
|
|
695
|
+
DbSkip()
|
|
696
|
+
EndDo
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**After (optimized):**
|
|
700
|
+
```advpl
|
|
701
|
+
// Break out of the loop when the key changes
|
|
702
|
+
Local cFilSD1 := xFilial("SD1")
|
|
703
|
+
|
|
704
|
+
DbSelectArea("SD1")
|
|
705
|
+
DbSetOrder(1)
|
|
706
|
+
DbSeek(cFilSD1 + cDoc + cSerie)
|
|
707
|
+
While !Eof() .And. SD1->D1_FILIAL == cFilSD1 ;
|
|
708
|
+
.And. SD1->D1_DOC == cDoc ;
|
|
709
|
+
.And. SD1->D1_SERIE == cSerie
|
|
710
|
+
nTotal += SD1->D1_TOTAL
|
|
711
|
+
DbSkip()
|
|
712
|
+
EndDo
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Avoiding Unnecessary Processing Inside Loops
|
|
716
|
+
|
|
717
|
+
**Before (slow):**
|
|
718
|
+
```advpl
|
|
719
|
+
// Formatting and string operations done even when not needed
|
|
720
|
+
While !Eof()
|
|
721
|
+
cLine := PadR(ALIAS->FIELD1, 20) + " | " + ;
|
|
722
|
+
Transform(ALIAS->FIELD2, "@E 999,999,999.99") + " | " + ;
|
|
723
|
+
DtoC(ALIAS->FIELD3)
|
|
724
|
+
|
|
725
|
+
If ALIAS->FIELD2 > nMinValue // only some records matter
|
|
726
|
+
aAdd(aResult, cLine)
|
|
727
|
+
EndIf
|
|
728
|
+
DbSkip()
|
|
729
|
+
EndDo
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**After (optimized):**
|
|
733
|
+
```advpl
|
|
734
|
+
// Filter first, format only matching records
|
|
735
|
+
While !Eof()
|
|
736
|
+
If ALIAS->FIELD2 > nMinValue
|
|
737
|
+
cLine := PadR(ALIAS->FIELD1, 20) + " | " + ;
|
|
738
|
+
Transform(ALIAS->FIELD2, "@E 999,999,999.99") + " | " + ;
|
|
739
|
+
DtoC(ALIAS->FIELD3)
|
|
740
|
+
aAdd(aResult, cLine)
|
|
741
|
+
EndIf
|
|
742
|
+
DbSkip()
|
|
743
|
+
EndDo
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
## Performance Checklist
|
|
749
|
+
|
|
750
|
+
Use this checklist when reviewing ADVPL code for performance:
|
|
751
|
+
|
|
752
|
+
| # | Check | Impact |
|
|
753
|
+
|---|-------|--------|
|
|
754
|
+
| 1 | Is the correct index selected with DbSetOrder? | High |
|
|
755
|
+
| 2 | Are SQL queries selecting only needed columns? | Medium |
|
|
756
|
+
| 3 | Is D_E_L_E_T_ filter included in SQL queries? | Medium |
|
|
757
|
+
| 4 | Are arrays pre-allocated when size is known? | Medium |
|
|
758
|
+
| 5 | Are objects freed with FreeObj when no longer needed? | Medium |
|
|
759
|
+
| 6 | Are temporary workareas (TCQuery aliases) closed after use? | High |
|
|
760
|
+
| 7 | Are loop invariants calculated outside the loop? | Medium |
|
|
761
|
+
| 8 | Do loops terminate as soon as the key changes? | High |
|
|
762
|
+
| 9 | Is GetMV/SuperGetMV cached before loops? | Medium |
|
|
763
|
+
| 10 | Are transactions scoped to only the necessary writes? | High |
|
|
764
|
+
| 11 | Is string concatenation avoided inside large loops? | Medium |
|
|
765
|
+
| 12 | Are REST/network calls batched instead of per-record? | High |
|
|
766
|
+
| 13 | Is the UI refreshed only at intervals, not every iteration? | Low |
|
|
767
|
+
| 14 | Are repeated DB lookups cached in arrays or static variables? | Medium |
|
|
768
|
+
| 15 | Is embedded SQL used for aggregations instead of ISAM loops? | High |
|