
BTP - Interface Performance
Which interface technology currently has the best performance when accessing On-Premise data in ABAP? Here we go into more detail.
Table of contents
In this article we look at the performance of different technologies and protocols and compare the runtime in different scenarios. At the end we look at the result and draw further conclusions from it.
Introduction
Have you ever wondered which technology is the best and fastest for accessing data in an on-premise system? There are currently various options in the SAP area for distributing or making data available. For example, there is the classic RFC, SOAP for more modern interfaces, the new standard REST and OData or IDoc for the distribution of documents and master data. Today we're looking at accessing on-premise data and the associated runtime.
On-Premise
Before we can start accessing, we first have to prepare our on-premise system. To do this, we create a table and make the data available to the outside world.
Table
We fill the table with different data and data types and use a UUID as the key. We also have randomly generated content and a string field with random character strings. Overall, the data should cover a wide variety of data.
@EndUserText.label : 'Performance Test'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zbs_dmo_perf {
key client : abap.clnt not null;
key identifier : sysuuid_x16 not null;
item_description : abap.char(40);
description : abap.char(150);
@Semantics.amount.currencyCode : 'zbs_dmo_perf.currency'
amount : abap.curr(15,2);
currency : abap.cuky;
blob : abap.string(0);
ndate : abap.dats;
ntime : abap.tims;
utc : abap.utclong;
}
Initialization
An executable class takes care of the data generation for us. We want to generate random data where the data sets are quite different. In this way, we want to avoid compression or condensation during data transfer if there are many identical data sets.
CLASS zcl_bs_demo_fill_performance DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
DATA randomizer_amount TYPE REF TO zcl_bs_demo_random.
DATA randomizer_currency TYPE REF TO zcl_bs_demo_random.
DATA randomizer_text TYPE REF TO zcl_bs_demo_random.
DATA randomizer_blob TYPE REF TO zcl_bs_demo_random.
DATA randomizer_date TYPE REF TO zcl_bs_demo_random.
DATA randomizer_time TYPE REF TO zcl_bs_demo_random.
DATA all_letters TYPE string.
METHODS initialize_class_attributes.
METHODS get_random_currency
RETURNING VALUE(result) TYPE waers.
METHODS get_description
RETURNING VALUE(result) TYPE zbs_dmo_perf-description.
METHODS get_blob
RETURNING VALUE(result) TYPE zbs_dmo_perf-blob.
METHODS get_letter
RETURNING VALUE(result) TYPE string.
METHODS get_date
RETURNING VALUE(result) TYPE d.
METHODS get_time
RETURNING VALUE(result) TYPE t.
ENDCLASS.
CLASS zcl_bs_demo_fill_performance IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA new_database_entries TYPE STANDARD TABLE OF zbs_dmo_perf WITH EMPTY KEY.
initialize_class_attributes( ).
DELETE FROM zbs_dmo_perf.
COMMIT WORK.
DO 50 TIMES.
DO 500 TIMES.
DATA(new_entry) = VALUE zbs_dmo_perf( identifier = xco_cp=>uuid( )->value
description = get_description( )
amount = randomizer_amount->rand( )
currency = get_random_currency( )
blob = get_blob( )
ndate = get_date( )
ntime = get_time( )
utc = utclong_current( ) ).
new_entry-item_description = |Item from { new_entry-ndate DATE = USER } in currency { new_entry-currency }|.
INSERT new_entry INTO TABLE new_database_entries.
ENDDO.
INSERT zbs_dmo_perf FROM TABLE @new_database_entries.
COMMIT WORK.
CLEAR new_database_entries.
ENDDO.
out->write( |New Entries created in ZBS_DMO_PERF| ).
ENDMETHOD.
METHOD initialize_class_attributes.
all_letters = to_lower( sy-abcde ) && to_upper( sy-abcde ).
randomizer_amount = NEW zcl_bs_demo_random( id_min = 2
id_max = 450 ).
randomizer_currency = NEW zcl_bs_demo_random( id_min = 1
id_max = 4 ).
randomizer_text = NEW zcl_bs_demo_random( id_min = 1
id_max = 52 ).
randomizer_blob = NEW zcl_bs_demo_random( id_min = 500
id_max = 2000 ).
randomizer_date = NEW zcl_bs_demo_random( id_min = 0
id_max = 730 ).
randomizer_time = NEW zcl_bs_demo_random( id_min = 0
id_max = 360 ).
ENDMETHOD.
METHOD get_random_currency.
CASE randomizer_currency->rand( ).
WHEN 1.
result = 'EUR'.
WHEN 2.
result = 'USD'.
WHEN 3.
result = 'RUB'.
WHEN 4.
result = 'CHF'.
ENDCASE.
ENDMETHOD.
METHOD get_description.
DO 150 TIMES.
result &&= get_letter( ).
ENDDO.
ENDMETHOD.
METHOD get_blob.
DO randomizer_blob->rand( ) TIMES.
result &&= get_letter( ).
ENDDO.
ENDMETHOD.
METHOD get_letter.
result = substring( val = all_letters
off = CONV i( randomizer_text->rand( ) - 1 )
len = 1 ).
ENDMETHOD.
METHOD get_date.
RETURN sy-datum - randomizer_date->rand( ).
ENDMETHOD.
METHOD get_time.
RETURN sy-uzeit - randomizer_time->rand( ) * 60.
ENDMETHOD.
ENDCLASS.
The class creates 25,000 data records, each in blocks of 500 units, which are passed to the database. This is to prevent us from passing on a very large number of entries in one step. After executing the class, we find our data in the table.
RFC function module
The first type of interface is a classic RFC function module. We pass a list of fields that we want to read and a TOP/SKIP to the module so that we can read parts of the data set. However, before you simply pass the generic list to the SELECT statement, you should check the contents. To do this, we use a standard class that performs the check for us.
FUNCTION z_bs_demo_get_performance_data
IMPORTING
VALUE(selection_fields) TYPE string_table
VALUE(top) TYPE i
VALUE(skip) TYPE i
EXPORTING
VALUE(performance_data) TYPE zbs_t_demo_performance.
DATA(fields_for_select) = ``.
LOOP AT selection_fields REFERENCE INTO DATA(field_name).
DATA(validated_name) = cl_abap_dyn_prg=>check_column_name( val = field_name->*
strict = abap_true ).
IF fields_for_select <> ``.
fields_for_select &&= `, `.
ENDIF.
fields_for_select &&= validated_name.
ENDLOOP.
IF fields_for_select IS INITIAL.
fields_for_select = `*`.
ENDIF.
SELECT FROM zbs_dmo_perf
FIELDS (fields_for_select)
ORDER BY identifier
INTO CORRESPONDING FIELDS OF TABLE @performance_data
UP TO @top ROWS
OFFSET @skip.
ENDFUNCTION.
Hint: In addition to checking the fields passed, in the case of an RFC function module, a sensible authorization check should also be carried out in order to protect the data (best practice).
OData Service
In the next step, we want to provide an OData service. We will look at the protocol version 2 and version 4. To do this, we create a basic view on our table and normalize the fields.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Base View Performance Data'
define view entity ZBS_B_DMOPerformance
as select from zbs_dmo_perf
{
key identifier as Identifier,
item_description as ItemDescription,
description as RandomDescription,
@Semantics.amount.currencyCode: 'Currency'
amount as Amount,
currency as Currency,
blob as BlobObject,
ndate as NewDate,
ntime as NewTime,
utc as UTCTimestamp
}
To do this, we generate a root view and then integrate it into a service definition. Since we do not need any behavior here, we save ourselves the trouble of defining and implementing the behavior.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Root Entity Performance'
define root view entity ZBS_R_DMOPerformance
as select from ZBS_B_DMOPerformance
{
key Identifier,
ItemDescription,
RandomDescription,
Amount,
Currency,
BlobObject,
NewDate,
NewTime,
UTCTimestamp
}
We then generate two service bindings and provide an OData API externally for the OData v2 and OData v4 protocols.
ABAP Environment
In the next step, we have to integrate the interfaces in the ABAP Environment and create corresponding objects (consumption model) for consumption. In all examples, we use the Destination Service in the BTP to access the data. The current best practice is to set up a communication arrangement and a communication system.
Function module
We use ACO_PROXY to get the definition of the function module in order to then import the metadata. You can find out how to integrate and call an RFC function module in this article. We then set up a consumption model using the metadata and build the call.
DATA(destination) = cl_rfc_destination_provider=>create_by_cloud_destination( destinations-rfc ).
DATA(consumption_model) = NEW zcl_bs_demo_intf_func( destination = destination ).
consumption_model->z_bs_demo_get_performance_data( EXPORTING selection_fields = request_data-fields
skip = request_data-skip
top = request_data-top
IMPORTING performance_data = result-data_rfc ).
OData
We use the service to save the metadata for both protocols with the addition "$metadata". You can find out how to integrate and consume an OData service in the ABAP environment in this article. We then implement the call for the two types of calls. These differ in the instantiation of the object, but are otherwise quite identical.
DATA(destination) = cl_http_destination_provider=>create_by_cloud_destination( destinations-http ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(client_proxy) = /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = 'ZBS_DEMO_INTF_O2'
proxy_model_version = '0001' )
io_http_client = http_client
iv_relative_service_root = '/sap/opu/odata/sap/ZBS_API_PERFROMANCE_O2/' ).
DATA(request) = client_proxy->create_resource_for_entity_set( 'PERFROMANCE' )->create_request_for_read( ).
request->set_top( request_data-top )->set_skip( request_data-skip ).
request->set_select_properties( request_data-fields ).
DATA(response) = request->execute( ).
response->get_business_data( IMPORTING et_business_data = result-data_o2 ).
Plain HTTP
As a final scenario, we implement a plain HTTP call, which means we do not use the consumption model or the gateway components in the system.
DATA(destination) = cl_http_destination_provider=>create_by_cloud_destination( destinations-http ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(fields_for_select) = ``.
LOOP AT request_data-fields REFERENCE INTO DATA(field_name).
IF fields_for_select <> ``.
fields_for_select &&= `,`.
ENDIF.
fields_for_select &&= field_name->*.
ENDLOOP.
DATA(uri) = `/sap/opu/odata4/sap/zbs_api_perfromance_o4/srvd_a2x/sap/zbs_demo_perfromance_data/0001/Perfromance?`.
uri &&= |$select={ fields_for_select }&$top={ request_data-top }&$skip={ request_data-skip }|.
DATA(request) = http_client->get_http_request( ).
request->set_uri_path( i_uri_path = uri ).
DATA(response) = http_client->execute( if_web_http_client=>get ).
DATA(status) = response->get_status( ).
IF status-code = 200.
/ui2/cl_json=>deserialize( EXPORTING json = response->get_text( )
CHANGING data = json_payload ).
result-http_payload = json_payload-value.
ENDIF.
The logic is longer here because we have to take care of everything ourselves. First we have to put together the URI and fill in the field list, as well as TOP and SKIP. Then we map the result to our internal structure. We are using OData v4 because it delivers JSON as a standard result.
Test
In this chapter we look at the different test scenarios and what we want to test.
Test cases
In our test we want to look at four access methods, via RFC with Consumption Model, OData v2 and OData v4 with Consumption Model, and via Plain HTTP, where we create the request and perform the mapping. To do this, we test various common scenarios for data acquisition:
- All fields and reading 100 entries
- All fields and reading 5000 entries
- Without BLOB (RandomDescription, BlobObject) and reading 5000 entries
- Without BLOB (RandomDescription, BlobObject), many executions and reading a block of 200 (same data records)
- Without BLOB (RandomDescription, BlobObject), many executions and reading blocks of 200
- Value help (key, description) and reading 500 entries
- Value help (key, description), many executions and reading from a block of 20
- Value help (key, description), many executions and reading from blocks of 20
Assumption
What assumption would we make in the test? Basically, we would say that RFC is still the faster protocol because less mapping, processing and checks have to be carried out during execution. On the other hand, plain HTTP should be faster than OData because a large ABAP layer is eliminated. The issue of performance between OData v2 and OData v4 is where things get interesting. Basically, OData v4 is transmitted as JSON, which makes the payload smaller.
Implementation
If you look in the repository, we use an HTTP endpoint to test the logic. This gives us the option of testing via principal propagation and basic authentication. Depending on how you want to use the destination service or which connection you want to test.
We divided the payload into various subtests, as we repeatedly found that individual runs were suddenly far outside the other runs. We did not look at the behavior in detail, but here is an example:
Results
Here are the summarized test runs. Three runs were carried out to get an average, so that you can also see the deviations described above. Apparently the phenomenon occurs with many calls to the HTTP framework.
Test cases 1 and 2:
Test cases 3 and 4:
Test cases 5 and 6:
Test cases 7 and 8:
Evaluation
What do we learn from the test and the test results? Here is a brief summary of the points from the test:
- RFC is still the simplest and fastest protocol when it comes to reading and moving data and quantities.
- A high frequency of HTTP queries leads to longer waiting times, so it is best to use fewer queries with large amounts of data.
- Plain HTTP calls are faster when reading and parsing than running through the OData integration.
- When calling the interface once, all technologies are quite close together and can be used easily.
- The implementation of HTTP and OData requires more source code to integrate simple reading actions.
Summary
In the past, there was the transfer of very large amounts of data, file processing or the direct call of RFC function modules, from which we could retrieve subsets of the data records. The current Clean Core strategy is moving towards OData and SOAP services. The recommendation to avoid classic interfaces such as RFC or IDoc should always be checked in performance-critical situations.
In principle, RFC is still the fastest protocol when it comes to simple transfer of data and content. The performance of the OData connection lies primarily in the wrapper layer, which lies between the HTTP framework and ABAP and handles the requests, but also contributes to stability and security.
Complete example
You can find the complete resources in various GitHub repositories, once for the On-Premise and once for the ABAP Environment part. You can find the unlisted helper classes in a separate repository, as we use them again and again in different projects.
Conclusion
With this test, we wanted to give you an insight into the topic of performance and how the current technologies compare to the classics. We did not look at events and SDA, in order to stick with the widely used techniques for direct access.