560 likes | 684 Views
TB-6 Coding Best Practices Tips, Tricks, Hints and Suggestions To Improve Your Code Tom Bascom, White Star Software Wednesday 12:30-13:30
E N D
TB-6 Coding Best Practices Tips, Tricks, Hints and Suggestions To Improve Your Code Tom Bascom, White Star Software Wednesday 12:30-13:30 Abstract: It is never too late to pick up a new idea that will improve your code. No matter what sort of code you write – Web, GUI, Character, or whatever, come to this session to hear Tips, Tricks, Hints and Suggestions for making your code better, faster and stronger than it was!
Best PracticesforOpenEdge Coding It isn’t just for Minty Fresh Breath! Tips, Tricks, Hints and Suggestions To Improve Your Code Tom Bascom, White Star Softwaretom@wss.com https://xkcd.com/844/
Readability is King There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies. — C.A.R. Hoare, The 1980 ACM Turing Award Lecture Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. Brian W. Kernighan and P. J. Plauger in The Elements of Programming Style.
Your Screen • In 1971 the first terminal screens with 24 lines and 80 green characters on a black background were introduced. • This was a big improvement • It is no longer 1971 • Your monitor is not a 7” diagonal phosphor tube. • It is much easier on your eyes to use a light background. • The Consolas fixed-width font is a lot prettier than COURIER. • THE CAPS-LOCK KEY IS STILL ON YOUR KEYBOARD but shouldn’t be used.
Open PuTTY • Select “Session” • Ensure that “Default Settings” is highlighted and click the “Load” button.
Define window width & height • Select “Window” • Set Columns to 160 • Set Rows to 48 (if you have a large screen larger values are fine)
Change the font • Select “Appearance” • “Consolas” is much easier on the eyes than “Courier” • If you have a small screen you may need a smaller font size like 9pt
Change the character set • Select “Translation” • (If you use UTF-8 line drawing will be funky)
Setup keyboard • Select “Terminal”, “Keyboard” • Backspace = Control-H • XtermR6 function keys
Change colors • Black backgrounds are so 1978… • Choose a lightbackground. • Set the defaultforeground toblack.
Save defaults • Ensure that “Default Settings” is highlighted • Click the “Save” button.
Your Screen (recap) • Being able to see and easily read code is critical! • Looking at a narrow window of code through a keyhole and with sunglasses on is not very effective. • Getting PuTTY properly setup is a stop-gap measure that will improve things in the short run. • Ultimately a tool like Progress Developer Studio is really where you want to be.
Good Naming • Focus on the business usage and meaning of the identifier. • Avoid negative names so that logical operations do not result in double negatives. • Avoid plural names, use the singular name, this will make looping more sensible. • “Hungarian notation” is an abomination: • DO NOT prefix identifiers with technical gobbledygook. • Even Microsoft now admits that this was a regrettable idea. • Exception: prefix TEMP-TABLE names with “tt”. • Exception: prefix buffer names with the usage of the buffer (i.e. “update” or “upd”) • Do NOT use hyphens (“-”) in names. • UPPERCASE is hideously ugly, lowerCamel case is best. • UPPERCASE also wears out the low order bits in RAM chips from over use.
Exception: UPPERCASE TRANSACTION • UPPERCASE TRANSACTION helps you to visualize where you intend a transaction to start. • Explicitly add the TRANSACTION keyword even if one will automatically start. • If this causes a compiler warning you almost certainly have a scope mismatch! • Fix the scope mismatch rather than removing the TRANSACTION keyword.
Comments • Make sure that comments are meaningful, accurate and add value. • Explain why you made an unusual choice. • Document alternatives that were rejected – and why. • Provide test cases for performance improvement claims. • Provide references to external documentation. • Do NOT comment out large blocks of code and thenleave them in the code base just in case you mightneed that code later: • That is what you have version control for. • Long comment blocks make your code substantially moredifficult to read and comprehend.
Spell Out Keywords • Abrvsmkur cd hrd 2 rd • Readability is critical to understandability and quality. • Abbreviations make searches more difficult and less reliable. • Use tableName.fieldName, this helps to distinguish database related identifiers from simple variables. • Do not abbreviate Progress keywords: • “def var” is an abbreviation – use “define variable”. • “int” and “char” are abbreviations – use “integer” and “character”. • etc.
NO-UNDO • UNDO processing ensures that database commits are consistent and complete. • Variables and TEMP-TABLES are local and do not need to be persisted to the db. • Unnecessary UNDO processing is a performance drag. • Variables and TEMP-TABLES should be defined with NO-UNDO. • It is extremely rare to find a legitimate use-case for an exception to this rule.
Use Properly Strong Scoped Updates • To have both a free reference and strong scoped references to a table you must have two distinct buffers: define buffer updCustomer for customer. find customer no-lock where custNum = 1 no-error. do for updCustomer TRANSACTION: find updCustomer exclusive-lock where custNum = 1 no-error. discount = 10. end. /* updCustomer record is not in scope and there is no SHARE-LOCK */
Verify Transaction and Buffer Scope ./trx3.p 12/18/2018 11:49:27 PROGRESS(R) Page 1{} Line Blk -- ---- --- 1 define buffer updCustomer for customer. 2 3 1 do for updCustomer TRANSACTION: 4 1 find updCustomer exclusive-lock where custNum = 1 no-error. 5 1 discount = 10. 6 end. File Name Line Blk. Type Tran Blk. Label -------------------- ---- ----------- ---- -------------------------------- ./trx3.p 0 Procedure No ./trx3.p 3 Do Yes Buffers: s2k.updCustomer The Transaction and the buffer are both scoped to the DO block. The DO block is a small and well contained block of update code. This is proper scoping.
Where to Put *-LOCK … • FIND and FOR EACH are flexible about the placement of NO-LOCK and EXCLUSIVE-LOCK: find loanfile where application# = app# no-lock no-wait no-error. vs find loanfile no-lock where application# = app# no-error. • The lock is related to the table – not the WHERE. • So it is more sensible to specify the lock right after the table name. • (Also – if there is NO-LOCK you won’t ever be waiting so NO-LOCK NO-WAIT is kind of silly...)
Borrowed Buffer Scope – Prevented! procedure b: define buffer customer for customer. /* this is perfectly legal */ find last customer no-lock. message “b” customer.custnum. pause. end. find first customer no-lock. message “start:” customer.custnum. pause. run b. message “end:” customer.custnum. /* no nasty side-effect! */ pause.
Temp-Tables, Locks, and Transactions • Temp-Tables are private to a session and are not persisted. • There are no record locks with temp-tables. • They do NOT participate in database transactions. • So they should not have a lock specified. • Nor should they have the TRANSACTION keyword applied to them. • (Some very old code may subscribe to a long deprecated idea that the TT might someday become a “real table”. Please do not copy that approach.)
“Chunking” Transactions define variable i as integer no-undo. outer: do for customer transaction while true: inner: do while true: i = i + 1. find next customer exclusive-lock. if not available customer then leave outer. discount = 0. if i modulo 100 = 0 then next outer. end. end. The performance of “chunked” updates can be substantially better than one record at time updates. This is generally true for UPDATES and CREATE but not for DELETE operations. Chunk sizes of more than a few hundred have diminishing returns. Iterating the outer loop commits a ”chunk” of transactions!
“Chunking” Transactions #2 The performance of “chunked” updates can be substantially better than one record at time updates. This is generally true for UPDATES and CREATE but not for DELETE operations. Chunk sizes of more than a few hundred have diminishing returns. define variable i as integer no-undo. input from "customer.d". outerLoop: do while true transaction: repeat: create customer. import customer. i = i + 1. if i modulo 100 = 0 then next outerLoop. end. leave outerLoop. end. input close. Iterating the outer loop commits a ”chunk” of transactions!
EMPTY TEMP-TABLE Slow vs for each ttSomething: delete ttSomething. end. empty temp-table ttSomething. The FOR EACH approach makes sense when you are only deleting a portion of the table. In which case you should have a WHERE clause.
Building Delimited Lists • The following is an elegant way to build a delimited list without testing for a trailing delimiter: define variable stateList as character no-undo. for each state no-lock: stateList = stateList + minimum( “,”, stateList ) + state.stateCode. end. This depends on every legitimate value sorting higher than “,”
Iterating Through Delimited Lists Obtain the number of entries in the list ONCE, outside the loop. Not with every iteration of the loop! define variable i as integer no-undo. define variable n as integer no-undo. define variable myList as character no-undo. myList = “ME,NH,VT,MA,CT,RI”. n = num-entries( myList ). do i = 1 to n: display entry( i, myList ). end. vs define variable i as integer no-undo. define variable myList as character no-undo. myList = “ME,NH,VT,MA,CT,RI”. do i = 1 to num-entries( myList ): display entry( i, myList ). end. NUM-ENTRIES() must scan the whole list every time it is called. If the list is long this is very, very expensive.
Use SUBSTITUTE instead of “+” for Strings • Avoids accidents with unknown values -- ? • Avoids calling string() to convert numeric and date data. • Simplifies adding spaces between elements in a template. define variable statusMsg as character no-undo format “x(70)”. statusMsg = “Loan# “ + xalias.jacket# + “ is in status “ + string( loanStat.xstatus ) + “ “ + string( today ). display statusMsg. statusMsg = substitute( “Loan# &1 is in status &2 &3“, xalias.jacket#, loanStat.xstatus, today ). display statusMsg.
ASSIGN • Group consecutive assignments into a single ASSIGN statement. • If possible ASSIGN indexed fields first. • This improves performance by reducing before-image notes and consolidating db updates. • Be careful not to accidentally add a period in the middle of the ASSIGN statement. vs loanAmount = 100000.00. loanTerm = 360. interestRate = 10.0. assign loanAmount = 100000.00 loanTerm = 360 interestRate = 10.0 . assign loanAmount = 100000.00 loanTerm = 360. interestRate = 10.0 .
CASE Statements • There was no CASE statement in Progress version 4… vs define variable choice as character no-undo. if choice = "a" then run this. else if choice = "b" then run that. else run something-else. define variable choice as character no-undo. case choice: when "a" or when “b” then run this. when "c" or when “d” then do: run that. run the/other/thing. end. otherwise run something-else. end case.
BY REFERENCE • By default the 4GL passes parameters BY VALUE: • This causes “deep copies” of TEMP-TABLE and DATASET objects. • Multiple deep copies are made if the object is passed among many layers. • This can require (very) large amounts of memory. procedure getUStats: define output parameter table for tt_usrTblInfo. /* even though it is a really a temp-table */ define output parameter table for tt_usrIdxInfo. /* we have to declare is as “table” */ /* do important stuff … */ end. run getUStats ( output table tt_usrTblInfoby-reference, output table tt_usrIdxInfoby-reference ).
COPY-LOB • By far the fastest and easiest way to read a file into a variable. • Gets around the “missing terminating linefeed” problem. • You can process a LOB line by line using entry(). define variable fileName as character no-undo format "x(30)". define variable textBody as longchar no-undo. define variable i as integer no-undo. define variable n as integer no-undo. update fileName. copy-lob from file fileName to textBody. n = num-entries( textBody, "~n" ). do i = 1 to n: display string( entry( i, textBody, "~n" )) format "x(80)" with frame a down. down with frame a. end.
Simple Things That Help Existing Queries • FIELDS – can reduce the size of records and thus allow more records to be packed into a message, i.e.FOR EACH customer FIELDS ( name balance ) NO-LOCK:or:FOR EACH customer EXCEPT ( photo ) NO-LOCK: • CACHE – client side buffer for records returned (default 50 - supposedly)DEFINE QUERY q FOR customer FIELDS ( name ) CACHE 1000
FORWARD-ONLY • Avoids building results sets for queries. • Improves performance when you do not need to move backward or sort. • The -noautoreslist client startup parameter applies to DEFINE QUERY and OPEN QUERY define variable qh as handle no-undo. define variable bh as handle no-undo. define variable ii as integer no-undo. create buffer bh for table "customer". create query qh. qh:set-buffers( bh ). qh:forward-only = true. qh:query-prepare( "for each customer" ). qh:query-open. qh:get-first(). do while qh:query-off-end = no: ii = ii + 1. qh:get-next(). end. display ii.
CACHE small, frequently used tables • Use for small, static, extremely frequently accessed tables • Often easily identified by the ProTop “turns” or “churn” metric • Can dramatically reduce network traffic and server workload • This kind of access is often 25% or more of db access! • GLOBAL SHARED TEMP-TABLE • Many legacy applications have a “common.i” in every program. • Give the TT the same name as the DB Table and the TT will be used. • Static CLASS / Singleton
TT Cache – The Old Fashioned Way /* common.i */ define new global shared temp-table location like location. /* initialize, if needed */ find first location no-error. /* yes, I said FIRST… */ if not available location then do: for each dictdb.location no-lock: create location. buffer-copy dictdb.location to location. end. end.
Add Instrumentation to Your Application! • ProTop Has Sample Code!
Sample Profiling Output ┌────────────────────────────────────────────── Profiler: Top 20 Results ───────────────────────────────┐ │Description: ProTop3 Execution Profile [00:00:23] │ │Session Total Execution Time 00:00:06 │ │Line 0 = initialization, line -1 = cleanup │ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ ┌───────────────────────────────────────── Top 20 Lines: Total Execution Time ──────────────────────────┐ │Program/Class Line Time Avg Time Calls Internal Procedure/Method │ │────────────────────────────── ───── ───────────── ───────────── ────────── ────────────────────────── │ │dc/dashboard.p 31053.702797 0.000001 4000004 mon-update │ │dc/dashboard.p 0 2.097173 0.524293 4 mon-update │ │dc/dashboard.p 3106 0.497054 0.000000 4000000 mon-update │ │dc/dashboard.p 3087 0.393956 0.004061 97 mon-update │ │lib/dynscreen.p 1238 0.025957 0.006489 4 dynScreenUpdate │ │ssg/sausage00.p 1949 0.024891 0.000007 3472 dataSet2JSON │ │lib/dynscreen.p 936 0.021612 0.000027 800 dynViewerUpdate │ │ssg/sausage00.p 1752 0.020572 0.000007 3084 scanDataSet │ │ssg/sausage00.p 1920 0.019844 0.000005 4252 dataSet2JSON │ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘
Aggregate Temp-Table Info ┌─────────────────────────── Temp-TableInfo ───────────────────────────┐ │ │ │ /home/pt3dev/tmp/DBI-9950762889q2d8B │ │ 1048576 DBIFile Size84 current temp-tables │ │ 1KBTT DB Block Size5 archived│ │ 1288 TT DB Total Blocks 125 peak│ │ 193 TT DB Empty Blocks 275 tt indexes│ │ 2 TT DB Free Blocks 1669 total current records│ │ 0 TT DB RM Free Blocks 109831 total current bytes│ │ │ │ 99.53% tt hit ratio │ │ │ │ 3225 tt rec create │ │ 34660 tt rec read │ │ 3032 tt rec update │ │ 85 tt rec delete │ │ 96186 tt rec log rd │ │ 453 tt rec os rd │ │ 1046 tt rec os wr │ │ 5376 tt TRX │ │ 64 tt Undos │ │ │ └───────────────────────────────────────────────────────────────────────┘
Detailed Temp-Table Info TT Name Procedure Name Bytes Records Create Read Update Del OSRd ────────────────────────────── ─────────────── ─────── ─────── ─────── ─────── ─────── ──── ──── tt_tbl protop.p5863 184 184 17145 9 3 tt_tbl.xid-idx protop.p 185 17801 tt_idx protop.p 10650 201 201 416 32 4 tt_idx.xid-idx protop.p 202 744 tt_screenElement lib/dynscreen.p34254 408 408 825 165 34 tt_screenElement.scrFrame lib/dynscreen.p 418 1409 tt_screenElement.elNm_frNm_elH lib/dynscreen.p 418 407 tt_browseColumnList lib/dynscreen.p 2701 65 65 989 37 4 tt_browseColumnList.brwCol lib/dynscreen.p 65 468 tt_browseColumnList.brwHdl lib/dynscreen.p 102 734
Top 5 User Tables & Indexes ┌─────────────── Top 5 Tables Used by My Session ───────────────┐ │tblName tblRd tblCr tblUp tblDl│ │──────────────────── ────────── ────────── ────────── ──────────│ │Order-Line 2,619 0 0 0│ │Customer 332 0 0 0│ │Order 207 0 0 0│ │Item 165 0 0 0│ │Salesrep 9 0 0 0│ └────────────────────────────────────────────────────────────────┘ ┌─────────────────── Top 5 Indexes Used by My Session ───────────────────┐ │idxName idxRd idxCr idxDl│ │──────────────────────────────────────── ────────── ────────── ──────────│ │Order-Line.order-line 2,622 0 0│ │_Field._Field-Name 2,191 0 0│ │_Index._File/Index 873 0 0│ │_File._File-Name 577 0 0│ │Customer.Cust-Num 336 0 0│ └─────────────────────────────────────────────────────────────────────────┘
Replacing Functions in WHERE Clauses define variable i as integer no-undo. define variable repList as character no-undo. /* repList might be a db field */ repList = "bbb,jal,rdr". /* manually populating it is for */ /* the convenience of the example */ for each customer no-lock where LOOKUP( salesRep, repList ) > 0: i = i + 1. end. display i. There are 373 customers meeting the criteria. This is a WHOLE-INDEX search so in the sports2000 database this will read all 1135 customer records!
Multiple OR Brackets define variable i as integer no-undo. for each customer no-lock where salesRep = "bbb” /* criteria are hard coded */ or salesRep = "jal" or salesRep = "rdr": i = i + 1. end. display i. The multiple OR brackets with equality matches allow this query to only read the correct records. But we had to hard-code the criteria.
Using an Outer Loop to Create an Equality Match define variable i as integer no-undo. define variable j as integer no-undo. define variable n as integer no-undo. define variable repList as character no-undo. /* this could be a db field */ repList = "bbb,jal,rdr". n = num-entries( repList ). /* do not recalculate n with */ /* every iteration of the loop! */ do j = 1 to n: for each customer no-lock where customer.salesRep = entry( j, repList ): i = i + 1. end. end. display i. We now have an equality match so the WHOLE-INDEX is gone. And the criteria are not hard-coded. But this structure only works well with relatively small numbers of entries in the list.
Using a TT Rather Than a Delimited List define temp-table tt_repList no-undo field repName as character index repName-idx is unique primary repName. . define variable i as integer no-undo. define variable n as integer no-undo. define variable repList as character no-undo. /* this could be a db field */ repList = "bbb,jal,rdr". /* build a TT for the join */ n = num-entries( repList ). /* do not recalculate n with */ do i = 1 to n: /* every iteration of the loop! */ create tt_repList. tt_repList.repName = entry( i, repList ). end. i = 0. for each tt_repList: for each customer no-lock where customer.salesRep = tt_repList.repName: i = i + 1. end. end. display i. Using the TT allows us to only read the correct records AND avoid hard-coding the criteria AND is much more flexible than a delimited list!