OpenEdge Development: Progress 4GL Handbook
![]() ![]() ![]()
|
Progress Programming Best PracticesHere you have arrived at the final chapter of this guide to programming in Progress. Along the way you have learned a number of factors that can improve the performance and the effectiveness of the procedures in your Progress 4GL applications. This chapter repeats and reinforces some of those points and introduces you to additional language constructs and programming techniques to help you build the best-performing applications you can.
This chapter includes the following sections:
Writing efficient procedures
There are many things you can do to maximize the efficiency of your Progress procedures and to avoid unnecessary overhead in your applications. This section discusses some of these.
Using NO-UNDO variables and temp-tables
When variables without the
NO-UNDOqualifier are updated, a before-image of the previous value is generated in what amounts to a separate record buffer for allNO-UNDOvariables. This is useful when you want the original value restored when youUNDOa block or transaction. When you do not need this capability, the before-image generation is needless overhead. Use ofNO-UNDOalso causes the Progress local before-image (.lbi) file, which is maintained for each user connected to a database, to be smaller. As noted in "Using Basic 4GL Constructs," when this guide first introduced theDEFINE VARIABLEstatement, it is unusual for variables to require theUNDOsupport that they receive by default. So, it is a good practice to make theNO-UNDOkeyword a standard part of your variable definitions unless a particular variable really needs the support.With temp-tables the situation is not always as clear. There might be cases where you are adding records to a temp-table or changing records in a temp-table within a transaction, and you want to be able to undo those changes. If this is not the case, however, you’ll benefit significantly from defining the temp-table with the same
NO-UNDOqualifier as for variables. This spares you from having Progress create a before-image of every temp-table change made within a transaction.Grouping assignments with the ASSIGN statement
If you are assigning two or more values in a row, it is significantly faster to assign them all in a single statement using the
ASSIGNkeyword. Even if the values being assigned have nothing to do with one another, it is faster to do it in a single statement, as in this example:
ASSIGN THIS-PROCEDURE:PRIVATE-DATA = STRING(OrderNum)hSource = SOURCE-PROCEDUREshipDate:BGCOLOR = dateColor(PromiseDate, ShipDate).
Here the code is setting an attribute on a procedure handle, a variable, and a field attribute. Even though these are unrelated, it is still best to
ASSIGNthem together.If you are assigning multiple field values within a single record buffer, the
ASSIGNstatement can be even more important. Progress adjusts index entries and does other work as part of each statement. If you assign two fields that participate in the same index in two separate statements, the index block is rebuilt once after each statement—a much greater overhead than doing it in one statement. In fact, because Progress assigns index entries at the end of each statement, you might even cause a temporary (but fatal) unique index violation if you assign part of a composite key in one statement and the other part in another. Don’t ever do this.Because of the efficiency of an
ASSIGNstatement without multiple assignments, some Progress developers always use theASSIGNkeyword even when the statement only does one assignment. There is no advantage to this. That is, these two statements are comparably fast:
Using BUFFER-COPY and BUFFER-COMPARE
When copying fields from one record buffer to another,
BUFFER-COPYdoes it more efficiently thanASSIGN. Use theEXCEPToption when some fields are not to be copied, and theASSIGNoption when some fields are renamed during the copy operation. Even doing aBUFFER-COPYof a table with 100 fields is faster than anASSIGNstatement that copies a half dozen of those fields.In particular, the static
BUFFER-COPYstatement allows Progress to resolve the exact field list to copy and to identify the field mappings at compile time so that the operation can be made as fast as possible. Using the dynamicBUFFER-COPYmethod on a buffer handle requires Progress to evaluate the arguments during program execution, which of course is slower. As with all dynamic language, use theBUFFER-COPYmethod only when you truly don’t know the buffers or the fields to copy until run time.All this applies equally well to the
BUFFER-COMPAREstatement or method which compares two buffers and returns a list of differences.BUFFER-COMPAREis much faster than using a series of explicit comparison statements on individual fields.Block-related tips
This section offers some advice on creating blocks of code.
Using DO instead of REPEAT
Back in "Procedure Blocks and Data Access," you learned about the various properties of different kinds of blocks. Remember that the
DOblock, by default, does not provide you with many of the default services that theREPEATblock does (for example, transaction management and default frame management). The flip side of this is that aDOblock is much faster than aREPEATblock if you don’t need these services. So, use a simpleDOblock whenever possible for any kind of iterating block that doesn’t need to manage a transaction or iterate through aDOWNframe.Minimizing block nesting
All blocks in Progress incur some overhead. Because Progress is a 4GL that provides many services to make programming easier and to make each statement do more than you’re used to from other languages, there is a cost to all blocks, even simple
DOblocks. For this reason, you should avoiding unnecessary block nesting wherever possible. Therefore, always use the form:
Instead of this form:
The AppBuilder always gives you a
DO ENDblock as a starting point for triggers, for instance. You should feel free to remove the block if your trigger requires only a single statement. And remember that there can be an additional benefit to combining multiple assignments into a single statement. If you turn several assignments into a singleASSIGNstatement, aDOblock that would otherwise have several statements can be reduced to just one statement with no block header.Minimizing nesting of procedure calls
Procedures, whether internal or external, are blocks as well, and relatively expensive ones. Obviously, you should use procedures as necessary to structure your application properly. However, if you run a relatively small procedure or invoke a function many times in a performance-sensitive loop, you should consider moving the code directly into the procedure that calls it to execute it inline. If it’s executed many times, this can make a significant difference in performance. If a procedure of this type is invoked from multiple places, you can make it into an include file and include it each place where it would otherwise be run. In this way, the code remains reusable but it is compiled directly in place wherever it is used. This can make the code much faster.
The CASE statement
If you have a sequence of
IF,THEN,ELSEclauses that all operate on different values of the same variable or expression, you can combine these into a single block using theCASEstatement, which has this syntax:
CASEexpression:{ WHENvalue[ OR WHENvalue] ... THEN{block|statement}} ...[ OTHERWISE{block|statement}]END [ CASE ].
The
expressioncan be a simple field or variable or any other expression involving multiple fields or values. Part of the optimization of theCASEstatement is that it evaluates the expression only once, when theCASEstatement is entered. By contrast, nestedIFstatements evaluate the expression in eachIFclause, even if the expression is the same each time.Following the block header are a number of
WHENclauses, each of which starts with avaluefor theexpression, followed byTHEN, followed by a statement or block to execute if the expression has that value. You can combine multipleWHENclauses withORif the same block or statement executes on multiple values of the expression.Finally, the
CASEblock can conclude with an optionalOTHERWISEclause with a statement or block to execute if the expression matches none of the values in theWHENclauses.The
CASEstatement is most useful when a variable or field can have one of a small number of possible values, and the procedure needs to react differently to each value.Here’s a simple example that uses the
CASEstatement to count the number of Orders of different types. There are four valid values for the OrderStatus field. TheOTHERWISEclause tallies any that don’t match any of the valid values (as it turns out, there aren’t any):
Figure 22–1 shows the result.
Figure 22–1: Result of CASE statement example
![]()
Using arrays instead of lists
This book cautions you against defining array fields with extents as part of your database definition, for a number of reasons. However, array variables, as well as array fields in temp-tables, can be very useful. One thing to keep in mind is that array element access is much faster than accessing an element of a list of comma-delimited strings. The
CASEexample above shows an example of this. The totals could have been accumulated within a single delimited list, but this would have involved scanning the list for the delimiters with each operation. The memory location of each element of an array is distinct and, therefore, quickly accessed.Using FOR FIRST versus FIND
The
FINDstatement retrieves a single record in a single statement. It has its limitations, however, when compared to aFORblock. AFINDstatement always uses only one index and does not allow use of field lists or word indexes. By contrast, aFORblock can use multiple indexes to evaluate a complexWHEREclause that involves fields not found in a single index. AFORblock can also use a word index and theCONTAINSkeyword to locate records, and supports the use of a field list to retrieve only a subset of the fields from the selected records. Both word indexes and field lists are briefly described in the "Using indexes properly" and the "Defining efficient queries and FOR EACH statements" . You can use the formFOR FIRSTwhen fetching just a single record, if any of the conditions apply that would give it a performance advantage. Of course, theFOR FIRSTconstruct involves creating a block, which has an overhead of its own that theFINDstatement doesn’t have. Therefore, it’s good practice not to useFOR FIRSTin all cases unless you have a specific reason.The ETIME function
You might be wondering (in fact, you should be wondering) how to tell which of two constructs is faster in your situation or how to measure the performance of a part of your application. Progress has a performance profiling tool (called Profiler) that can show you exactly how much of your processing time is going to what routines, but there is also a much simpler way to do a test of a specific part of your code: the
ETIMEfunction (for elapsed time).ETIMEreturns an integer value representing the number of milliseconds since the function was reset to zero. Because a millisecond is a substantial amount of time on a modern processor, you often have to repeat an action many times inside a loop to measure accurately just what its cost is.To reset the counter that
ETIMEuses, you invoke the function with an argument value of yes or true. Otherwise, you invoke it with no argument and no parentheses. If you forget to reset it to zero before you start,ETIMEreturns some enormous integer representing the number of milliseconds since your session started.This example shows you whether it is faster to use the
FINDstatement or aFOR FIRSTblock to find the Order with a Customer Number of 24 and an Order Date of 1/31/98.If you look at the indexes for the Order table in the Data Dictionary, you see that there is an index on the Order Date field, and another index that has the CustNum field as its primary component, followed by the OrderNum field. The
FOR FIRSTconstruct takes advantage of both of these indexes to resolve the request in the most efficient way possible. TheFINDstatement can use only one of the indexes.The code first resets
ETIME, then does the sameFINDin a loop 10000 times. It then saves theETIMEcounter for this operation. Then it resets it again, does aFOR FIRST10000 times, saves that value, and finally displays both values:
Figure 22–2 shows the result.
Figure 22–2: Result of ETIME function example
![]()
So, the
FOR FIRSTwas significantly faster. The actual difference is very dependent on the actual indexes and the number of records to search.Using the CAN-FIND function
The
CAN-FINDfunction lets you determine whether a record with certain field values exists, for example as part of field validation.CAN-FINDtakes a buffer name and aWHEREclause just as you would write for aFINDstatement. It returns true if the record exists and false otherwise. Just as with theFINDstatement, theCAN-FINDfunction can identify a unique record satisfying theWHEREclause, or you can include theFIRST(or evenLAST) keyword before the buffer name to determine whether at least one record exists that satisfies theWHEREclause. TheCAN-FINDfunction returns false if more than one record satisfies the selection criteria and you do not include the qualifierFIRSTorLAST.This simple procedure shows two uses of
CAN-FIND:
In the first case, the code looks for Orders whose CustNum field doesn’t match any Customer record. This code would cause an error in the database’s referential integrity.
The second block looks for Customers that have no Orders. This code would probably be a valid condition, since you need to add a Customer before you start adding Orders for it.
As it turns out, the Sports2000 database doesn’t have any invalid Orders without Customers. If you add a few extra Customers to the database that aren’t in the standard database and that don’t have any Orders, the
DISPLAYstatement will show those, as you can see in Figure 22–3.Figure 22–3: Result of CAN-FIND function example
![]()
Why does the Progress 4GL have this function in addition to the
FINDstatement?CAN-FINDcan return true or false simply by looking at the index entries if the selection criteria can be resolved strictly through a single index, without having to retrieve record values. After all, the statement is only asking whether a record exists (that is, if it can be found). It is not retrieving any particular field values.Thus,
CAN-FINDis beneficial and efficient only when you use it to identify records through one or more fields in a single index. IfCAN-FINDhas to retrieve the database records themselves, then you have lost its performance advantage over theFINDstatement.Dynamic programming tips
This section provides some advice on programming with dynamic objects.
Dynamic versus static programming
The most basic thing to consider about programming with dynamic objects is whether and when to do it at all. Since support for dynamic objects was added to the Progress 4GL in more recent releases, some developers think that dynamic constructs are inherently an improvement over their static counterparts and are tempted to program everything using dynamic objects. This is a big mistake. Dynamic objects are intended to give you more choices when you develop an application, not to replace static references to tables and fields. There are several important considerations:
- Generally, dynamic constructs execute more slowly than their static equivalents. This is logical because, at compile time, Progress cannot anticipate what the procedure will do at run time and so cannot set up the structures to support the procedure. More of the work is done at run time, when the interpreter looks at the values of the dynamic procedure elements, just in time to prepare and execute them.
- Dynamic programming is more difficult than static programming. Code that is full of dynamic references is usually more difficult to understand at first glance. Since important values are stored in variables rather than hard-coded into the source procedures, it is less clear what is going on when you first look at the source code. Also, the use of handles and other devices that provide you with a level of indirection makes it harder to follow what a procedure is doing without studying it carefully. It is especially important here that you provide good internal documentation in the form of comments.
- You will usually want to write your business logic in static form, with specific references to the tables and fields (or their temp-table equivalents). Your business logic is likely to be unique to a single situation, rather than something repeated many times throughout your application. You can’t write static business logic against dynamically defined objects.
So consider the power of dynamic programming for those situations where your application is doing the same thing in many places. Rather than writing many variations on the same source procedure or creating many compiled versions of the same procedure using include files or preprocessors, you can create a single procedure that does the same general job for every part of your application that needs it. This is why dynamic constructs exist. Consider their cost and the responsibility of having to clean up after yourself when you use them. Where they are the right thing to use, they can add tremendous flexibility to your application and dramatically reduce the number of different procedures you have to maintain.
Reusing dynamic objects
The major section of this chapter discusses Progress memory management and emphasizes that you must delete any dynamic objects you create. It’s important to keep in mind, however, that if you are going to create another dynamic object of the same type, especially inside a loop that is executed many times, it is much more efficient to reuse the same dynamic object rather than deleting it and re-creating it. This is true even if you change all the attributes of the object. Then you must simply remember to delete it after you are completely done with it.
For example, the following procedure needs to generate a dynamic buffer and a dynamic query for every table in the Sports2000 database. The information about tables, fields, and indexes is actually stored as schema information in the database itself, and you can access it the same way you do any other information. There are just a few bits of information you need to know to understand this example:
- A record for each table is stored in a table called
_file.- The name of the file is in a field called
_file-name.- Each table has a number called the
_file-num.- There are special tables in the database that have file numbers either less than zero or greater than 32K, so the procedure skips those.
For each database table, the code creates a dynamic query and a dynamic buffer, and sets the query’s buffer to the dynamic buffer handle. Then the code prepares a default query for the table, opens it, gets the first record, and closes it. It then deletes the dynamic query and the dynamic buffer. To show the effect of the performance more dramatically, this is all done inside a loop 100 times. Figure 22–4 shows how long it took for one sample run.
Figure 22–4: Result of DELETE OBJECT example
![]()
There’s a more efficient way to do this. If you move the
CREATE QUERY hQuerystatement before theDOblock and theFOR EACHblock, and you move theDELETE OBJECT hQuerystatement after the end of those blocks, the procedure runs somewhat faster. Even though you are resetting the query’s buffer and re-preparing it for a completely different buffer, it is still faster to reuse the same dynamic object:
Figure 22–5 shows the result.
Figure 22–5: Result of more efficient DELETE OBJECT example
![]()
Just be sure you don’t forget to delete the object when you’re done with it! Unless you’re reusing the object a large number of times, the difference in performance won’t be dramatic, so it isn’t worth the risk of forgetting to delete a dynamic object unless you will be reusing it many times in succession.
Note that you can’t reuse the dynamic buffer in the same way. When you use the
CREATE BUFFERstatement, you must name the table the buffer will be for. This name can be an expression, as it is here, so that the buffer name can be assigned dynamically at run time, but you can’t then reuse that same dynamic buffer object for a different buffer. So you have to create it and delete it inside the loop.Using indexes properly
A detailed discussion of index design is beyond the scope of this book. But this section offers a few guidelines that relate directly to how proper use of indexes can contribute to the performance of your application.
Using word indexes for status indicators
A word index is a special index type that you can define for a character field. It indexes not the entire field value, as a normal index would, but every individual word in the index. There are delimiters you can define to tell the database manager just what you would like to see treated as a word, what the delimiters between words are, and so forth. You can learn about word indexes in OpenEdge Development: Programming Interfaces. Word indexes can be a tremendously powerful mechanism for identifying database fields that contain particular words. In fact, there is a special
CONTAINSoperator for aWHEREclause, similar toBEGINSandMATCHES, which is reserved for use with word indexes.One powerful use of word indexes is not just to provide an index on all the words in a free text field, such as a status message or customer comments, but to create special character fields in which you store strings that identify other aspects of the record. For example, you can create a character field for a table in which you store various attributes of the record that otherwise would be individual logical fields with true/false values. It’s much more efficient to use the
CONTAINSoperator on a word-indexed field than to evaluate a number of different indexed fields. You can also store some combination of field names and field values in a word-indexed field to make it easier and faster to find a record based on a number of different search criteria, such as customers where you have some particular bits and pieces of name and address information.Avoiding indexes on logical values
If your application needs to identify records that satisfy some Boolean condition (such as Active vs. Inactive, Male vs. Female, or Domestic vs. Foreign), it is not a good idea to do this by means of indexes on Logical fields that represent the two conditions. The same is true of other fields that have only a handful of values, whether they are character values, such as Foreign and Domestic, or integer values representing those meanings. An index bracket is the portion of the index that the OpenEdge RDBMS must search through to identify all the records that match your selection criteria. If this is half or a large fraction of all the records, then the index is not serving its purpose and data retrieval is not efficient. Instead, you should consider encoding these kinds of values in a word-indexed character field. Under very special circumstances it might be beneficial to define an index on a logical value when, for example, 99 percent of the records are true for that value and you frequently need to identify the one percent that are false.
Using multi-component indexes
You can define an index on one or more fields in a table. Defining a multi-component index can be much more effective than defining multiple indexes on the same individual fields, but only when your application needs to access that combination of fields in the order in which they appear in the index. For example, if your application sometimes needs to select data based on the value of field A, and sometimes on A and B together, and sometimes on A, B, and C, then it makes sense to define a multi-component index with fields A, B, and C in that order.
However, if your application sometimes needs to select data based just on field B, or on C, or on B and C together, without knowing the value of A, then this index will do you no good any more than you can easily locate a word in the dictionary by knowing the second or third letter in the word.
Always evaluate the selection requirements of your application carefully as you design your database indexes.
Avoiding unneeded indexes
Another danger is simply defining too many indexes on a table. You should define an index for a table when you have most or all of these requirements:
- The index greatly reduces the amount of data to search to locate needed records. You should avoid indexes on small numbers of distinct values.
- The index is needed frequently. If only one occasionally used procedure needs some unusual selection, it is probably not worth defining an index just for that case.
- Fast performance is essential for the procedure that uses the index. If you have a batch report that runs once a month that needs some special selection criteria, it probably isn’t worth defining an index just for that purpose.
Maintaining an index every time you create or update a record is relatively expensive. Maintaining many indexes on the same table can be very expensive. Avoid defining indexes you don’t really need.
Hiding screen contents when making changes
Various types of operations make a series of changes to the user interface of an application before the user interface arrives at a final state. This might involve calculating a succession of values that are visible on the screen or adjusting the state of objects, such as combo boxes and selection lists. If you set the objects’ (or where appropriate the frame’s)
HIDDENattribute to true while you are making the adjustments, not only will you avoid flashing on the screen as things are changing, but the changes generally execute faster because the screen does not have to refresh after each adjustment.Remember that the browse has a special
REFRESHABLEattribute that you can set to false while you are making changes to the data displayed in the browse, to also prevent flashing for that object. This avoids having the browse hidden during the calculations, which might be distracting, but effectively freezes it so that the user sees only the final result of the changes.Defining efficient queries and FOR EACH statements
This section provides some tips for defining queries and
FOR EACHstatements.Using field lists
Progress allows you to specify a reduced list of fields to retrieve when you define a query or start a
FOR EACHblock. This is the syntax for theDEFINE QUERYfield list:
DEFINE QUERYquery-nameFORbuffer-name{ FIELDSfield... | EXCEPTfield... } [,buffer-name... ].
This is the syntax for the
FOR EACHfield list:
If you specify a list of
FIELDSfor a buffer, only those fields are retrieved. If you specify anEXCEPTlist, only those fields are not retrieved.Under some circumstances, using a field list can reduce the amount of data transferred across the network in a client/server environment. However, there are serious limitations to the field list that mean that you should have limited use for it in most modern OpenEdge applications:
- The field list option was primarily designed for use with OpenEdge DataServers, which provide a connection to non-OpenEdge databases, such as Oracle and SQL Server. These kinds of databases typically have fixed-length data values that can be much larger than their OpenEdge counterparts, where all data is stored in an efficient, variable-length form.
- The field list has an effect only in a client/server environment, where your client application session has a direct connection to a database server on another system. This is not the recommended architecture for any new applications. A truly distributed application uses an AppServer to run an OpenEdge session that accesses the database and returns data to the client using temp-tables, as has been extensively discussed in this book. In this environment, you are completely in control of what fields you pass between client and server through your temp-table definitions. The field list mechanism plays no role in this.
- Even if you have a database connection in a client/server environment, Progress always retrieves the entire record if you lock the record with an
EXCLUSIVE-LOCK.- Progress retrieves additional fields beyond those in the field list for its own purposes, including evaluating some of the selection criteria of the query or
FOR EACH.- You must remember that the field list is not the same as a display list for a browse or a field list for the fields in a frame. You define the
DISPLAYlist of columns in a browse independently of the query that the browse uses. If you inadvertently leave out a field in theFIELDSlist of a query definition that is needed by any part of the application that uses the query, your application will generate an error at run time. This can cause serious maintenance problems if your query definitions must explicitly name every field that is used from that query anywhere on the client.The bottom line here is that in a distributed application, you control the field list through the definition of temp-tables that pass data from server to client, and the
FIELDSphrase on a query definition is not needed as part of that definition.Structuring your selection criteria in a join
When you need to retrieve data from multiple joined tables in a single query or
FOR EACHstatement, it is important to put the tables into the proper sequence and to specify your selection criteria as early in the retrieval process as possible.OpenEdge does not optimize complex joins in the same way that some other database managers do, rearranging the order of tables and fields. There is a very good reason for this. Because Progress is designed to make it easy and effective to deal with individual records and multiple levels of selection, rearranging a join in a single statement is not typically an issue. For example, this kind of nesting of data retrieval blocks is very typical in Progress business logic:
In this kind of code, the developer understands the order in which data is retrieved and is relying on that order to structure the business logic for related tables.
If you join tables in a single statement, Progress retrieves the data in the order you specify. Progress does not second-guess your selection and rearrange the retrieval for you. This means that you have to take responsibility for structuring your selection efficiently. For example, if you want to retrieve orders processed today for Customers with a CreditLimit, this kind of statement is very inefficient in Progress:
/* Less efficient selection: */FOR EACH Customer WHERE Customer.CreditLimit NE 0,EACH Order OF Customer WHERE OrderDate = TODAY:
Hardly any Customers have a CreditLimit of 0, so the Customer selection is going to return nearly all Customers. On the other hand, only a few Customers have placed Orders today. It would be much more efficient to identify the Orders first, and then get the Customer for each of those Orders:
/* More efficient selection: */FOR EACH Order WHERE OrderDate = TODAY,FIRST Customer OF Order WHERE CreditLimit EQ 0:
It’s especially important to place the selection criteria for each table as high up in the statement (that is, as close to the front) as possible. Always define the selection for each table as part of the phrase for that table’s buffer. That is, don’t write a statement such as this:
/* Inefficient selection: */FOR EACH Customer,EACH Order OF Customer WHERE Customer.CreditLimit NE 0 AND OrderDate = TODAY:
In this case, Progress does just what you ask it to do:
- Retrieves each Customer in the Customer table in turn, into the Customer buffer, regardless of its CreditLimit or anything else.
- Retrieves each Order for each Customer in turn, into the Order buffer.
- Examines the CreditLimit value in the Customer buffer to see if it equals 0.
- If the CreditLimit does not equal zero, examines the OrderDate in the Order buffer to see if it’s equal to today’s date.
This is clearly a very inefficient way to go through the data, especially because there is an index on the OrderDate field and another index on the CustNum field in both the Order table and the Customer table that allows Progress to identify those Orders and their Customers immediately.
If you are used to working with other data retrieval languages, you might miss the optimization of complex queries that they do, but Progress gives you control over your application behavior by presenting you with the data you ask for in the way that you ask for it. When you are writing real business logic this is much more useful than having the DBMS evaluate some complex set of expressions on a
WHEREclause that joins multiple tables and return a single processed result.Copying temp-tables as parameters
"Defining and Using Temp-tables," describes how you can pass temp-tables from one procedure to another using the static
TABLEparameter form, the dynamicTABLE-HANDLEparameter form, or simply by passing theHANDLEof the temp-table. It is extremely important that you pass temp-tables using the simpleHANDLEparameter whenever possible, if your procedure call is within the same session. When you pass the handle to a temp-table, as with any object, you are simply passing a pointer to its location elsewhere in the session. If you pass the temp-table using either theTABLEorTABLE-HANDLEform, you are forcing Progress to perform a complete copy of the structure of the temp-table and the data in the table. You need to do this only if the procedure receiving the temp-table needs a static definition of the table. Especially when the procedure receiving the temp-table is only an intermediary, which simply passes the temp-table on to some other procedure, there is never a need to pass the table itself, only its handle. Observing this guideline whenever you use temp-tables can greatly improve your application’s performance.Setting your Propath correctly
The Propath is the set of directories Progress looks in to locate procedure files to run. Configuring your Propath correctly is essential to good application performance. The more directories there are on the Propath, the longer it takes to search for files. Larger directories may take longer to search than smaller ones, depending on your operating system.
You should order directories by their frequency of use. Put the most frequently used directories first.
Database and AppServer-related issues
This section describes issues related to the OpenEdge database and the AppServer.
Avoiding unnecessary database connections
Connecting to a database is a relatively slow operation that should be done only once. Do not connect and disconnect over and over. Specify the databases to be connected on the command line rather than using the
CONNECTstatement in the 4GL. If necessary, you can specify database connections when you enter significant modules that use that database, but be aware of the cost.Minimizing creation of subprocesses
Creating a new process is an expensive operation that can be very time consuming if done often and especially when done by multiple users on the same system. Use statements like
INPUT THROUGH,OUTPUT THROUGH, andUNIXwith care.Using database sequences
"Updating Your Database and Writing Triggers," you saw how you can use database sequences to generate a sequence of unique integer values, for example, as key values for a database field such as a Customer Number or Order Number. Sequences are much faster than using an integer field in a control record stored in the database, and they do not cause lock conflicts as a control record does. To obtain unique values from a field in a database record, each user needs to get an
EXCLUSIVE-LOCKon the record within a transaction, retrieve its value, increment the value, and then end the transaction and release the record. This is very time consuming, especially when you consider that it is likely that many users will be trying to do this at the same time. This is exactly the purpose that database sequences were designed for. When you execute theNEXT-VALUEfunction on a sequence, the OpenEdge RDBMS increments the sequence and returns the value to you in a single operation, so that there is no chance that another user can see the same value, but without any need for a transaction.Minimizing the size and number of network messages
The two slowest operations in most systems are transmitting data over a network and accessing data on a disk. When using AppServers, sockets, or client/server database access, take care to minimize the number of network calls you make. Whenever possible, send large amounts of data at once rather than many small transmissions.
Always remember to test on slow networks. A dial-up connection is many times slower than a fast LAN connection. Network messages take considerably longer to transmit on slow connections. Sometimes an application works well during development because a fast network is used and fails miserably when deployed in the real world.
Network latency is limited and is generally beyond your control. The speed of light limits network transmission time. For example, it is 3266 miles from Boston to London. Via the Internet, it might be 5000 miles or more. A sample routing chart shows 18 hops from a PC in a Boston office to a server in a London office. It takes at least 40 milliseconds to send a message, assuming best possible conditions. Typical conditions might require 80 or 100 milliseconds. So, sending a message and getting a response takes around 160 milliseconds if the server responds quickly. At that rate, you can send and receive 6 (typical) to 12 (best case) messages per second. Nothing will ever make this turnaround time faster. Technical improvements in the Internet will only allow you to send more data in the same time.
Configuring your session using startup options
Progress supports a large number of startup options that you can use, among other things, to tune the configuration of your session, controlling memory allocation for various purposes and other important factors that can have a large effect on performance. A detailed discussion of these is beyond the scope of this book, but this section mentions a few key ones.
Using the -q startup option
The Quick Request (
-q) option avoids constant file lookups to determine if a previously loaded.rfile needs to be replaced in memory by a newer one. If you specify–q, then the Propath search is done only once, on the initial load of the.rand not on every invocation of it. You should always use the–qoption except when you are in development mode and regularly changing and compiling application procedures.Increasing the –Bt startup option size
The
-Btstartup option sets the buffer size for temporary tables. The default value is very small, only 10 buffers. Increasing the buffer size to 100 or more can be useful.Increasing the –Mm startup option size
In client/server environments, increasing the maximum size of the buffers used for network messages allows more data per message to be sent. This is especially beneficial when reading groups of records with
NO-LOCK. The size is a maximum and smaller messages are sent when there is not enough to completely fill the buffer. The default size is 1024 bytes. Increase it to 16384. Not that the same size must be specified on both the client and server side, and that the server allocates message buffers for each client. Large buffers consume more server memory. The increase on the client side is not significant.Memory management in Progress
The chapters on dynamic objects taught you that even in a high-level language like the Progress 4GL, you have to do your own memory management when you use dynamic objects. The basic rule is very simple: You create it, you delete it!
This section re-enforces this basic concept and gives you some examples of things you need to be aware of and techniques to use to make sure that your application doesn’t sprout memory leaks that bring it to a halt when you put it into production.
You shouldn’t treat the need for memory management as a reason to avoid programming using dynamic objects. Remember that the Progress 4GL started out as a strictly static language, where everything was defined in the procedural source code and the compiler resolved every table and field reference. Dynamic objects have been added to the language precisely because they can make your development much more effective, and allow you to reuse procedures and logic, when you need a single operation on different tables, fields, or other objects at different times. You should take full advantage of dynamic objects. You just need to remember to clean up after yourself.
The need for careful programming is especially important because many memory management problems only show up when you run your application over long periods of time in production, and the effects of even small memory leaks cause drastic problems. That is definitely not the time for you to be discovering the problems in your application! A little discipline up front protects you from this.
Cleaning up dynamically allocated memory
When you run a procedure with only static objects, these objects come and go with the procedure that defines them. Because Progress can see the individual
DEFINEstatements for the objects, it knows when to create them, when they go out of scope, and when to delete them. You don’t have to worry about deleting them because Progress does it automatically for you.The same is true, incidentally, of handles for static objects. When you store the handle of a static object in a
HANDLEvariable, that value is just a pointer to the object. It doesn’t extend or affect the scope of the static object in any way. The variable itself, because it’s defined statically, is also deleted by Progress when its procedure goes out of scope.When you create dynamic objects, however, Progress cannot control their scope or their lifetime. If you create a dynamic object inside a loop, Progress might have no way of knowing at compile time how many of those objects the procedure will create. If you pass the handle of an object as a parameter to another procedure, Progress has no way of knowing at compile time where the handle came from, what it will be used for, or when the object it represents can safely be deleted. This is why you are responsible for taking care of this.
To see a dramatic example of the importance of memory management:
- Open the
h-DeleteObject.pprocedure used earlier and comment out the statements that delete the dynamic query and buffer objects at the end of the loop:
- Bring up the Windows Task Manager, select the Processes tab, and find the running executable called
prowin32.exe:
![]()
- Run the procedure, and watch the memory go:
After just 100 iterations of this loop, which only created two dynamic objects, you’ve lost six megabytes of memory! Not only that, but even though you removed two key statements from the procedure, it ran about four times slower:
The reason for this is that all that extra memory allocation is getting in the way of your procedure running efficiently. So you pay a heavy price all the way around.Use theDELETE OBJECTstatement to get rid of the memory for any dynamic object, regardless of its type. To summarize, you use aCREATE QUERYstatement to create a dynamic query, aCREATE BUTTONstatement to create a dynamic button, and aCREATE BROWSEstatement to create a dynamic browse, but you use theDELETE OBJECTstatement to clean up each of them.You must also remember to clean up persistent procedures when you’re done with them. Every time you execute a statement of theRUNprocedure-namePERSISTENT SETproc-handleform, you are also allocating memory for the procedure and all its contents. You need to delete the procedure, when you’re done with it, with theDELETE PROCEDUREstatement.Using widget pools
Deleting individual dynamic objects is a big responsibility, and a serious nuisance as well. Widget pools are designed to help you make your use of dynamic objects much simpler and more reliable.
A widget pool provides a means of treating objects you create as a set. When you delete the pool, all the objects you created in it go away together. The simplest way to group objects using widget pools is to associate all the objects in a single procedure with a widget pool. In fact, the template procedures used by the AppBuilder for windows (and for visual SmartObjects, such as SmartWindows and SmartDataViewers) help you do this by having a
CREATE WIDGET-POOLstatement in the template’s definitions section:
This means that, by default, all the dynamic objects you create that go into the window are deleted when the window is closed and its procedure deleted. This includes not just visual objects but dynamic buffers, queries, and so forth.
Remember that a simple
CREATE WIDGET-POOLstatement creates an unnamed widget pool. This pool automatically goes away when its procedure terminates. This might not always be the behavior you want. In fact, by passing the handles to dynamic objects around, you can easily wind up with a handle whose scope exceeds the lifetime of the dynamic object it points to. In this case, the value of the handle can become invalid.Here’s a simple example of how this can happen. The
h-MakeBuffer.pprocedure has an internal procedure calledgetBufferthat creates a dynamic buffer and returns it to the caller. TheCREATE WIDGET-POOLstatement puts all such buffers into an unnamed widget pool for that procedure:
Another procedure,
h-RunMakeBuff.p, runsh-MakeBuffer.pPERSISTENTand then runsgetBuffer:
The first message statement, as expected, correctly displays the name of the dynamic buffer that was returned, as shown in Figure 22–6.
Figure 22–6: Result of widget pool example
![]()
What happens, though, when the procedure has deleted its instance of
h-MakeBuffer.p?The unnamed widget pool in that instance of
h-MakeBuffer.pgoes away, and the dynamic buffer thathMyBufpoints to goes away with it. But thehMyBufvariable is still very much alive, and in fact the value of the handle that it holds hasn’t changed. However, that handle value doesn’t point to anything anymore, so you get a string of errors, as shown in Figure 22–7.Figure 22–7: Errors for unnamed widget pool example
![]()
This sequence of errors is the bane of any dynamic programmer’s existence. It is the most likely consequence of failing to make sure that all your dynamic objects live exactly as long as they need to, but no longer.
Using named widget pools
The solution to this is to give names to widget pools that hold objects that might outlive the procedure that created them. In addition to giving the pool a name, you must also define it to be
PERSISTENT. You can then create dynamic objects explicitly in that pool:
The persistent widget pool becomes another dynamic object that your application has to take responsibility for managing. You must delete it when you’re finally done with, or all the objects in it will sit there in memory until your session ends. Here the calling procedure cleans up after it is done using the buffer handle:
/* … end of h-RunMakeBuff.p … */DELETE PROCEDURE hProc.MESSAGE " No more errors, the buffer is still"hMyBuf:NAME VIEW-AS ALERT-BOX.DELETE WIDGET-POOL "MakeBuffPool".
When you run the procedure, the errors go away as shown in Figure 22–8, because the handle is valid until you delete the pool.
Figure 22–8: Result of named widget pool example
![]()
Remember that all these steps are important:
- You must give the widget pool a name if you want to allocate objects to it specifically or if you want it to outlive the procedure that creates it.
- The widget pool name is a character expression. So, if you are using a literal string rather than a variable for the name, you must remember to put it in quotes both where you create it and wherever you reference it. This is different from other
CREATEstatements, where you normally specify a handle variable as a target for the create and where the dynamic object you create doesn’t really have a name the same way that static objects do.- You must also make a named pool
PERSISTENTif you want it to outlive the procedure that creates it.- You must remember to delete the widget pool when you are done using it, just as you delete individual dynamic objects that aren’t in a specific widget pool when you are done with them.
You might create a named widget pool that was not persistent simply to put different dynamic objects in different pools within a single procedure, so that you could delete one widget pool within the procedure without deleting objects in some other widget pool. In this case, all of the pools that haven’t been specifically deleted during the execution of the procedure are deleted when the procedure is deleted.
A widget pool cannot be created as
PERSISTENTwithout giving it a name.When you create a persistent named pool, its name effectively becomes global to the session. Any procedure running anywhere in the session can delete it. You cannot, however, access named widget pools through the
SESSIONhandle as you can with some other kinds of objects, such as windows and procedures.Making the best use of dynamic objects
There are a few simple rules for making the best use of dynamic objects without incurring memory leaks:
- Keep your interactions short. The longer the span between when you create an object and when your application is done using it, the greater the likelihood that you will forget to delete it and that you will never detect this until your application dies in production because of a memory leak. This span really has less to do with time than with the organization of your procedures. If the architecture of your application is clear about how dynamic objects are created and when they are deleted, then you will do well. If your code is inconsistent about this, then you will have a very difficult time identifying whether you’ve cleaned up after yourself or not.
- Always use widget pools. Remember that the
CREATE WIDGET-POOLstatement at the top of a procedure is not a Progress default, or even a default for all procedures you build in the AppBuilder. It is simply a convention observed by a few template files for some kinds of procedures the AppBuilder creates. Create your own templates and your own convention and stick to it.- Make a practice of always deleting every object as soon as you are done with it, even if you are using widget pools. A widget pool in a procedure can accumulate an enormous number of unused objects if you wait until the procedure goes away or until the widget pool is explicitly deleted. The widget pool mostly serves as a backup mechanism to get rid of objects you somehow forget to delete explicitly or to help you organize the deletion of related groups of objects. If you have a procedure that creates a large number of objects and then deletes them all at once, then go ahead and use the
DELETE WIDGET-POOLstatement for that purpose. In that case, be as conscientious about deleting every widget pool as soon as you are done with it as you should be with individual objects.- If you create named persistent widget pools, be very clear in your application architecture about making sure that some procedure deletes them, unless they are used only for objects that always must live for the duration of the session. Also, make sure that you never delete a pool while its objects are still being used.
- If your procedure is immediately going to create another object of the same type as one you are done with, then go ahead and reuse the same object without deleting it and creating a new one. This is generally faster, even if you are changing all the characteristics of the object. But don’t leave piles of objects lying around in your code on the off chance that you might want to reuse them.
- Test your application rigorously with an eye to memory usage before you ship it to your customers.
Avoiding stale handles
You saw the errors that you get if you inadvertently try to reference (or dereference, as the error message says) a stale handle. A stale handle is one that holds a pointer to an object that no longer exists. If you make sure that your objects always live just as long as they should, then you shouldn’t get this type of error. But it’s still very good practice to check the validity of a handle when there’s any chance that it might be invalid, which almost certainly includes whenever it comes from another procedure that might be maintained independently of the one that needs to use the handle.
You can use the
VALID-HANDLEfunction to check whether a handle is valid. If you do use this check, then always determine how your procedure should react if a handle that ought to be valid turns out not to be. Simply substituting your own “Invalid handle!” message for the default one that Progress gives you is not the right approach. There are many things you can try to blame the end user of your application for, but supplying a procedure with an invalid object handle is not one of them.Remember, too, that once you have encountered an invalid handle, whether it is a stale handle for a deleted object or simply a handle variable that was never properly initialized, your end user will get another ugly message for every statement in your procedure that vainly tries to reference it. In the
Runmake.pexample, the procedure generated three internal system errors because the buffer handle was not valid. It could easily have been a hundred errors, if the procedure had gone on to continue to try to work with that invalid handle. Never risk subjecting your end users to this abuse.The
VALID-HANDLEfunction is also very useful and necessary anytime your code is walking through a chain of handles, such as in this procedure that displays all the windows currently running in the session:
You could also use the
VALID-HANDLEfunction as a signal that tells your procedure whether an attempt to locate a particular object was successful or not, somewhat like theAVAILABLEfunction after aFINDstatement on a buffer.In early versions of Progress, it was possible (and quite likely) that if you deleted an object and then created another object, even an object of a different type, the new object would have the same handle value as the object you just deleted. Therefore, the
VALID-HANDLEfunction could return true because the handle was pointing to a valid object, even though it was not the object you expected it to be. TheTYPEandUNIQUE-IDobject attributes exist largely to help you identify whether an object is the same one you referred to earlier. As of Progress Version 9.1C, this is no longer the case. Handles are allocated in such a way that you cannot have a handle variable that inadvertently points to a different object residing in the same memory location as one that was deleted earlier in the session. Therefore, you can be confident that when you use theVALID-HANDLEfunction, it is not only telling whether the object the handle points to is some valid object, but assuring you that it is the same object it always was before. Progress does this using a mechanism called opaque handles. You do not need to do anything special to get the benefit of them.Deleting persistent procedures
You must clean up persistent procedure instances you have created just as with other objects. Remember that the
RUNprog.pPERSISTENTstatement is much the same asCREATEobject. Progress creates a running instance of the procedure and all its contents, and leaves it in memory until you specifically delete it. The simplest way to delete a procedure when you’re done with it is to use this statement:
Note that you use the
PROCEDUREkeyword in this statement, notOBJECT.The AppBuilder templates observe a convention that is a very useful alternative to simply deleting a procedure. If you delete a procedure from the outside, it might not be finished doing what it needs to do and might not have a chance to clean up the objects inside it properly. It is much more reliable, and much more object-oriented, to tell the procedure to delete itself rather than to simply kill it. You’ve seen the way the AppBuilder-generated procedure handled this earlier, but it’s worth repeating in the context of this discussion about memory management.
The procedure’s main block runs a standard internal procedure called
enable_UIand then waits for aCLOSEevent if the procedure is not being run persistent. If it is being run persistent, then it stays in memory without the need for anyWAIT-FORstatement:
The
WINDOW-CLOSEevent on the window applies the proceduralCLOSEevent to the procedure:
DO:/* This event will close the window and terminate the procedure. */APPLY "CLOSE":U TO THIS-PROCEDURE.RETURN NO-APPLY.END.
The main block also defines this
CLOSEevent, which runs the cleanup code in thedisable_UIinternal procedure, which again is part of the AppBuilder’s own standard mechanism for starting and stopping procedures:
/* The CLOSE event can be used from inside or outside the procedure to *//* terminate it. */ON CLOSE OF THIS-PROCEDURERUN disable_UI.
And finally,
disable_UIdeletes any dynamic objects. Then, if the procedure was run persistent, it explicitly deletes itself. If it wasn’t run persistent, then Progress deletes it for you:
Whether you use the AppBuilder templates or not, use some similar convention that makes sure your procedures clean up after themselves and deletes them when your application is done with them.
Deleting temp-table copies
When you pass a static temp-table from one procedure to another using the static
TABLEparameter form, either locally or remotely, Progress takes care of deleting the temp-table and its contents when the procedure that defines it is deleted, just as it does for other static objects.However, if you pass a temp-table through a
TABLE-HANDLE, then you are creating a dynamic copy of the temp-table. Whether there is a static temp-table definition for it on one side of the call or the other, theTABLE-HANDLEparameter creates a dynamic temp-table. If a procedure passes a temp-table to another procedure using theINPUT TABLE-HANDLEform, then the calling procedure must take responsibility for deleting the temp-table when it’s done with it. If a procedure receives a temp-table from another procedure using theOUTPUT TABLE-HANDLEform, then the receiving procedure must delete its copy of the temp-table after it is done with it.As with other dynamic objects, Progress does not know when you are done with a dynamic copy of a temp-table and therefore cannot delete it for you. You cannot allocate a temp-table to a widget pool, so this mechanism does not help you out. The temp-table is created in the
SESSIONwidget pool and therefore stays there until it’s deleted or until the session ends. Because a temp-table is normally a much larger object than a dynamic button, query, or buffer, it is critically important that you delete temp-tables when you’re done with them. If you’re done with the data in the temp-table but need to use the table again, then use theEMPTY-TEMP-TABLEmethod on the table handle to remove the data. Delete the table itself using theDELETE OBJECTstatement when you are done with it altogether.Because dynamic temp-tables are allocated in the
SESSIONpool, you can locate them through theSESSIONhandle’s buffer chain. If the buffer has a validTABLE-HANDLEattribute, then it’s a temp-table buffer and itsTABLE-HANDLEpoints to the temp-table itself:
Other object types
Memory management is also an issue for other object types that aren’t discussed in this book, but which this section mentions briefly:
- MEMPTR — A
MEMPTRis a pointer to an area of your computer’s memory that you allocate and use independently of any Progress objects. You are fully responsible for controlling this memory. You allocate memory using theSET-SIZEfunction. You deallocate it by usingSET-SIZEto set the size to zero. There are other functions that manipulate memory allocated with aMEMPTR, but Progress does not recognize the form of the contents in any way.- ActiveX control — You can extend the visual content of your user interface and other aspects of an application that runs on MS Windows using ActiveX controls. You access an ActiveX control through a special handle called a
COM-HANDLE. You are responsible for using a specialRELEASE OBJECTstatement to release memory associated with the object. Because ActiveX controls operate outside the bounds of objects known to Progress, and because they can do so much on their own, they can be a major source of memory leaks. You need to use and test them very carefully.- Socket — A socket is a connection to a process running outside the OpenEdge session, possibly on another machine. OpenEdge provides access to other procedures through its support for sockets. A
SOCKETis another kind of dynamic object you can create and use from the 4GL. You must clean up sockets just like any other objects. There is a specialSESSION:FIRST-SOCKETattribute that lets you access the head of the chain of all the sockets allocated in your session.- Asynchronous request — You can run a procedure in another OpenEdge session with a special
ASYNCHRONOUSkeyword to allow your session to continue to run until the other session responds. A procedure run in this way is kept in memory even after the request is complete so that you check the completion status. You must take special care to delete these procedures when you are done with them. There is also aSESSION:FIRST-ASYNC-REQUESTattribute that identifies the chain of all outstanding asynchronous requests in your session.There is a certain amount of information on these special types in OpenEdge Development: Progress 4GL Reference and online help, but you will find detailed information in OpenEdge Development: Programming Interfaces.
Conclusion
Memory management in Progress is not that difficult if you establish a standard set of templates for your procedures and an architecture that makes it clear what procedures are responsible for creating dynamic objects and deleting them, and when your session is done using them. If you are organized in your design and test your application carefully, you should have no trouble creating a reliable application that makes appropriate use of the power and flexibility of dynamic objects. Just remember the basic rule:
You create it, you delete it!
Conclusion to the book
This book has introduced you to the essentials of the Progress 4GL. Its purpose is to give you the skills you need to start to build successful business applications using the OpenEdge platform. Even in a book of this scope, it is not possible to discuss all the elements of the Progress 4GL. Language statements that support interfaces to other programming environments, such as socket support, support for generating and consuming XML documents, and others, are left to other documentation. Also, language statements used in constructing Progress-based reports and statements directly in support of the database and AppServer are discussed elsewhere. In addition, the many new features of OpenEdge Release 10 are covered in other documentation.
In any case, the language statements alone are never sufficient to build a complex enterprise application. It’s very important that you adopt an overall architecture for your application to give you the consistency and flexibility you need to complete your application and to maintain and extend it in a fast-changing world. OpenEdge provides components such as Progress SmartObjects, as well as the Progress Dynamics development framework to give you a basis for creating powerful and sophisticated applications quickly. Other OpenEdge 10 documentation also provides guidelines and principles for an overall application architecture. Whether you use OpenEdge-provided components or develop your own framework, you should plan the structure of your application carefully before you begin writing large numbers of Progress 4GL procedures. A thorough up-front design will save you tremendous effort in the long run and give you a basis for an application that will ensure your success long into the future.
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |
![]() ![]() ![]()
|