@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.
Files changed (70) hide show
  1. package/agents/changelog-generator.md +63 -0
  2. package/agents/code-generator.md +215 -0
  3. package/agents/code-reviewer.md +145 -0
  4. package/agents/debugger.md +83 -0
  5. package/agents/doc-generator.md +67 -0
  6. package/agents/docs-reference.md +86 -0
  7. package/agents/migrator.md +84 -0
  8. package/agents/process-consultant.md +97 -0
  9. package/agents/refactorer.md +75 -0
  10. package/agents/sx-configurator.md +67 -0
  11. package/commands/changelog.md +66 -0
  12. package/commands/diagnose.md +67 -0
  13. package/commands/docs.md +81 -0
  14. package/commands/document.md +67 -0
  15. package/commands/explain.md +60 -0
  16. package/commands/generate.md +111 -0
  17. package/commands/migrate.md +81 -0
  18. package/commands/process.md +111 -0
  19. package/commands/refactor.md +65 -0
  20. package/commands/review.md +60 -0
  21. package/commands/sxgen.md +98 -0
  22. package/commands/test.md +103 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +143 -0
  26. package/dist/index.js.map +1 -0
  27. package/package.json +30 -0
  28. package/skills/advpl-code-generation/SKILL.md +163 -0
  29. package/skills/advpl-code-generation/patterns-fwformbrowse.md +485 -0
  30. package/skills/advpl-code-generation/patterns-jobs.md +519 -0
  31. package/skills/advpl-code-generation/patterns-mvc.md +765 -0
  32. package/skills/advpl-code-generation/patterns-pontos-entrada.md +708 -0
  33. package/skills/advpl-code-generation/patterns-rest.md +974 -0
  34. package/skills/advpl-code-generation/patterns-soap.md +639 -0
  35. package/skills/advpl-code-generation/patterns-treport.md +481 -0
  36. package/skills/advpl-code-generation/patterns-workflow.md +779 -0
  37. package/skills/advpl-code-generation/templates-classes.md +1096 -0
  38. package/skills/advpl-code-review/SKILL.md +72 -0
  39. package/skills/advpl-code-review/rules-best-practices.md +444 -0
  40. package/skills/advpl-code-review/rules-modernization.md +290 -0
  41. package/skills/advpl-code-review/rules-performance.md +333 -0
  42. package/skills/advpl-code-review/rules-security.md +302 -0
  43. package/skills/advpl-debugging/SKILL.md +265 -0
  44. package/skills/advpl-debugging/common-errors.md +1124 -0
  45. package/skills/advpl-debugging/performance-tips.md +768 -0
  46. package/skills/advpl-refactoring/SKILL.md +139 -0
  47. package/skills/advpl-to-tlpp-migration/SKILL.md +293 -0
  48. package/skills/advpl-to-tlpp-migration/migration-checklist.md +122 -0
  49. package/skills/advpl-to-tlpp-migration/migration-rules.md +265 -0
  50. package/skills/changelog-patterns/SKILL.md +99 -0
  51. package/skills/code-explanation/SKILL.md +66 -0
  52. package/skills/documentation-patterns/SKILL.md +172 -0
  53. package/skills/embedded-sql/SKILL.md +379 -0
  54. package/skills/probat-testing/SKILL.md +226 -0
  55. package/skills/probat-testing/patterns-unit-tests.md +614 -0
  56. package/skills/protheus-business/SKILL.md +92 -0
  57. package/skills/protheus-business/modulo-compras.md +780 -0
  58. package/skills/protheus-business/modulo-contabilidade.md +874 -0
  59. package/skills/protheus-business/modulo-estoque.md +876 -0
  60. package/skills/protheus-business/modulo-faturamento.md +800 -0
  61. package/skills/protheus-business/modulo-financeiro.md +1015 -0
  62. package/skills/protheus-business/modulo-fiscal.md +749 -0
  63. package/skills/protheus-business/modulo-manutencao.md +848 -0
  64. package/skills/protheus-business/modulo-pcp.md +743 -0
  65. package/skills/protheus-reference/SKILL.md +119 -0
  66. package/skills/protheus-reference/native-functions.md +7029 -0
  67. package/skills/protheus-reference/rest-api-reference.md +1758 -0
  68. package/skills/protheus-reference/restricted-functions.md +265 -0
  69. package/skills/protheus-reference/sx-dictionary.md +854 -0
  70. 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 |