6
0
mirror of https://github.com/FirebirdSQL/firebird-qa.git synced 2025-01-22 21:43:06 +01:00

Added/Updated bugs\core_0733_test.py. Fully re-implemented. Checked on 5.0.0.591, 4.0.1.2692, 3.0.8.33535 - both on Windows and Linux.

This commit is contained in:
zotov 2022-08-04 16:07:36 +03:00
parent 6efb2db675
commit c5923862a4

View File

@ -5,343 +5,230 @@ ID: issue-1108
ISSUE: 1108
TITLE: Compress Data over the Network
DESCRIPTION:
Results are completely opposite to those which were obtained on snapshots when this test
was implenmented (3.0.5.33084, 4.0.0.1347). Requirement to compress data leads to DEGRADATION
of performance when data are stored on local machine, and we have no ability to change
storage when fbt_run is in work (at least for nowadays).
After discuss with dimitr it was decided to remove any logging and its analysis.
We only verify matching of RDB$GET_CONTEXT('SYSTEM', 'WIRE_COMPRESSED') and value that
was stored in the firebird.conf for current check.
Test creates two tables with similar DDL (single varchar column) and fills data into them.
First table ('t_common_text') gets textual data which can be compressed in extremely high degree.
Second table ('t_binary_data') will store GUID-data which is obviously incompressable.
Then we perform <N_MEASURES> runs with selecting data from each of these tables, and we store
result of *deltas* of CPU user time that was 'captured' before and after cursor fetch all rows.
Within each measurement we gather info TWO times, by changing WireCompression = true vs false
(this is client-side parameter and it must be defined in DPB; see 'db_cfg_object' variable).
### NOTE ###
Changed value of parameter WireCompression (in firebird.conf) will be seen by application
if it reloads client library. Reconnect is NOT enough for this. For this reason we use
subprocess and call ISQL utility to do above mentioned actions in new execution context.
Results of <N_MEASURES> for each value of WireCompression and each source (t_common_text vs t_binary_data)
are accumulated in the lists, and MEDIAN (of that CPU time values) is evaluated after all serie completed.
Following ratios are compared in this test with apropriate 'thresholds':
1) how CPU UserTime depends on WireCompression if we send 'IDEALLY COMPRESSABLE' textual data over network.
If WireCompression is ON (and actually works) then CPU time must be GREATER then if WireCompression = OFF.
Minimal threshold for that case is tuned by 'MIN_CPU_RATIO_TXT_WCOMPR_ON_OFF' setting;
2) same as "1", but when we send 'ABSOLUTELY IMCOMPRESSABLE' binary data over network.
Minimal threshold for that case is tuned by 'MIN_CPU_RATIO_BIN_WCOMPR_ON_OFF' setting;
3) how CPU userTime depends on NATURE of sending data ('ideally compressable' vs 'absolutely imcompressable')
if WireCompression is OFF. This comparison is not mandatory for this test purpose, but it can be useful
for estimation of how record-level compression works.
Ideally compresable text occupies much less data pages this CPU usertime for processing must be LESS then
for imcompressible binary data.
Maximal ratio between them must be more than 1, see setting 'MAX_CPU_RATIO_TXT2BIN_WCOMPR_OFF'.
We have to compare RATIOS between these CPU time medians with some statistics which was collected beforhand.
Ten runs was done for Windows and Linux, values of thresholds see in the source below.
See also tests for:
CORE-5536 - checks that field mon$wire_compressed actually exists in MON$ATTACHMENTS table;
CORE-5913 - checks that built-in rdb$get_context('SYSTEM','WIRE_ENCRYPTED') is avaliable;
NOTES:
[9.11.2021] pcisar
[09.11.2021] pcisar
This test was fragile from start, usualy lefts behind resources and requires
temporary changes to firebird.conf on run-time. It's questionable whether
wire-compression should be tested at all.
[04.08.2022] pzotov
Fully re-implemented (see description). No more datediff() for time measurement, CPU UserTime is analyzed.
CAUTION.
DO NOT set DATA_WIDTH less than 1'500 and N_ROWS less then 10'000
otherwise ratios can be unreal because of CPU UserTime = 0.
Checked on 5.0.0.591, 4.0.1.2692, 3.0.8.33535 - both on Windows and Linux.
JIRA: CORE-733
FBTEST: bugs.core_0733
"""
import psutil
import platform
from collections import defaultdict
import pytest
from firebird.qa import *
from firebird.driver import driver_config, connect
init_script = """
create domain dm_dump varchar(32700) character set none;
recreate table t_log( required_value varchar(5), actual_value varchar(5), elap_ms int );
commit;
#--------------------------------------------------------------------
def median(lst):
n = len(lst)
s = sorted(lst)
return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None
#--------------------------------------------------------------------
N_MEASURES = 9
DATA_WIDTH = 10000 # 1500 # 8200 #1600
N_ROWS = 15000
# MINIMAL ratio between CPU user time when WireCompression = true vs false
# and we operate with 'IDEALLY COMPRESSABLE' (textual) data.
# Windows: 10.000; 13.330; 10.250; 14.000; 8.000; 9.750; 8.400; 13.667; 10.000 // 4.0.1
# 8.600; 8.800; 8.800; 7.167; 11.000; 8.600; 8.800; 15.000; 7.167 // 3.0.8
# Linux: 7.538; 6.733; 6.266; 6.125; 7.692; 7.538; 8.083; 7.143; 8.083 // 4.0.1
# 7.384; 7.071; 6.999; 6.733; 6.714; 8.083; 6.467; 7.308; 7.538 // 3.0.8
MIN_CPU_RATIO_TXT_WCOMPR_ON_OFF = 5 if platform.system() == 'Linux' else 6
# MINIMAL ratio between CPU user time when WireCompression = true vs false
# and we operate with 'ABSOLUTELY IMCOMPRESSIBLE' data.
# Windows: 5.750; 9.000; 6.286; 6.285; 6.286; 7.167; 5.500; 6.286; 6.285 // 4.0.1
# 4.909; 3.714; 3.643; 4.417 3.000; 4.384; 3.600; 3.533; 4.500 // 3.0.8
# Linux: 5.158; 6.466; 6.333; 6.533; 6.667; 6.063; 6.267; 5.500; 5.875 // 4.0.1
# 6.643; 7.000; 6.333; 6.267; 7.385; 5.812; 6.063; 6.643; 6.500 // 3.0.8
MIN_CPU_RATIO_BIN_WCOMPR_ON_OFF = 4 if platform.system() == 'Linux' else 4
# OPTIONAL CHECK: minimal ratio between CPU user time when WireCompression = false
# and engine sends to client: 1) 'IDEALLY COMPRESSABLE'; 2) 'ABSOLUTELY IMCOMPRESSIBLE' data.
# This can be useful to estimate record-level compression: textual data can be compressed
# very well, so its transfer must take much less time then for GEN_UUID.
# Experiments show that this ratio can be on Windows in the scope 0.3.... 0.8,
# but on Linux it can be 1.0 or even slightly more then 1(!)
# Windows: 0.500; 0.600; 0.571; 0.428; 0.714; 0.667; 0.625; 0.429; 0.571 // 4.0.1
# 0.454; 0.357; 0.357; 0.500; 0.250; 0.385; 0.333; 0.200; 0.500 // 3.0.8
# Linux: 0.684; 1.000; 1.000; 1.067; 0.867; 0.813; 0.800; 0.777; 0.750 // 4.0.1
# 0.928; 1.000; 0.933; 1.000; 1.077; 0.750; 0.938; 0.929; 0.928 // 3.0.8
MAX_CPU_RATIO_TXT2BIN_WCOMPR_OFF = 1.1 if platform.system() == 'Linux' else 0.95
init_script = f"""
create domain dm_dump varchar({DATA_WIDTH}) character set octets;
create table t_common_text(s dm_dump);
create table t_binary_data(s dm_dump);
set term ^;
create or alter procedure sp_uuid(a_compressable boolean, n_limit int default 1)
returns (b dm_dump) as
declare g char(16) character set octets;
execute block as
declare i int = {N_ROWS};
begin
if ( a_compressable ) then
while (n_limit > 0) do
begin
g = gen_uuid();
b = lpad('',32700, 'AAAAAAAAAAAAAAAA' );
n_limit = n_limit - 1;
suspend;
end
else
while (n_limit > 0) do
begin
b = lpad('',32700, gen_uuid() );
n_limit = n_limit - 1;
suspend;
end
while (i > 0)do
begin
insert into t_common_text(s) values( lpad('',{DATA_WIDTH}, 'A') );
insert into t_binary_data(s) values( lpad(_octets '',{DATA_WIDTH}, gen_uuid()) );
i = i - 1;
end
end
^
create or alter procedure sp_emitter(a_compressable boolean, n_limit int default 1)
returns (b dm_dump) as
begin
while (n_limit > 0) do
begin
b = lpad('',{DATA_WIDTH}, iif(a_compressable, 'A', uuid_to_char(gen_uuid())));
n_limit = n_limit - 1;
suspend;
end
end
^
set term ;^
commit;
"""
db = db_factory(init=init_script)
db = db_factory(init=init_script, charset = 'none')
act = python_act('db')
expected_stdout = """
RESULT_OF_REQ_COMPARE_TO_ACTUAL EXPECTED: actual values were equal to required.
"""
@pytest.mark.skip("FIXME: Test fate to be determined")
@pytest.mark.version('>=3.0')
def test_1():
pytest.fail("Not IMPLEMENTED")
def test_1(act: Action, capsys):
# ORIGINAL fbtest test_script
#---
#
# import os
# import time as tm
# import datetime
# from time import time
# import re
# import shutil
# import subprocess
# import platform
#
# from fdb import services
#
# os.environ["ISC_USER"] = user_name
# os.environ["ISC_PASSWORD"] = user_password
#
# DB_NAME = '$(DATABASE_LOCATION)' + 'bugs.core_0733.fdb'
#
# DB_PATH = '$(DATABASE_LOCATION)'
# U_NAME = user_name
# U_PSWD = user_password
# NUL_DEVICE = 'nul' if platform.system() == 'Windows' else '/dev/null'
#
# N_ROWS = 1
#
# F_SQL_NAME=os.path.join(context['temp_directory'],'tmp_core_0733.sql')
#
# fb_home = services.connect(host='localhost', user= user_name, password= user_password).get_home_directory()
# dts = datetime.datetime.now().strftime("%y%m%d_%H%M%S")
# fbconf_bak = fb_home+'firebird_'+dts+'.tmp_0733.bak'
# shutil.copy2( fb_home+'firebird.conf', fbconf_bak )
#
# db_conn.close()
#
# #--------------------------------------------
#
# def flush_and_close(file_handle):
# # https://docs.python.org/2/library/os.html#os.fsync
# # If you're starting with a Python file object f,
# # first do f.flush(), and
# # then do os.fsync(f.fileno()), to ensure that all internal buffers associated with f are written to disk.
# global os
#
# file_handle.flush()
# if file_handle.mode not in ('r', 'rb'):
# # otherwise: "OSError: [Errno 9] Bad file descriptor"!
# os.fsync(file_handle.fileno())
# file_handle.close()
#
# #--------------------------------------------
#
# def prepare_fb_conf( fb_home, a_required_value ):
#
# f_fbconf=open(fb_home+'firebird.conf','r')
# fbconf_content=f_fbconf.readlines()
# f_fbconf.close()
# for i,s in enumerate( fbconf_content ):
# if s.lower().lstrip().startswith( 'wirecompression'.lower() ):
# fbconf_content[i] = '# <temply commented> ' + s
#
# fbconf_content.append('\\n# Temporarily added by fbtest, CORE-0733. Should be removed auto:')
# fbconf_content.append("\\n#" + '='*30 )
# fbconf_content.append('\\nWireCompression = %s' % a_required_value )
# fbconf_content.append("\\n#" + '='*30 )
# fbconf_content.append("\\n" )
#
# f_fbconf=open(fb_home+'firebird.conf','w')
# f_fbconf.writelines( fbconf_content )
# f_fbconf.close()
# #------------------------------------------------------------------------------------
#
# def prepare_sql_4run( required_compression, db_path, n_rows, sql_file_name ):
# global os
# global U_NAME
# global U_PSWD
# global NUL_DEVICE
#
# sql_dump='tmp_core_0733_compression_%(required_compression)s.dump' % ( locals() )
#
# if os.path.isfile( '%(db_path)s%(sql_dump)s' % (locals()) ):
# os.remove( '%(db_path)s%(sql_dump)s' % (locals()) )
#
# if n_rows is None:
# return
#
# #------------------
#
# sql_text='''
# set list on;
#
# set term ^;
# execute block returns(dts timestamp) as
# begin
# dts = 'now';
# rdb$set_context('USER_SESSION','DTS_BEG', dts);
# suspend;
# end
# ^
# set term ;^
#
# out %(NUL_DEVICE)s;
#
# set term ^;
# execute block returns(b dm_dump) as
# begin
# /***********
# for
# execute statement 'select b from sp_uuid( true, %(n_rows)s )'
# on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME')
# as user '%(U_NAME)s' password '%(U_PSWD)s'
# into b
# do
# suspend;
# ***********/
# end
# ^
# set term ;^
# out;
#
# set term ^;
# execute block returns(dts timestamp) as
# begin
# dts = 'now';
# rdb$set_context('USER_SESSION','DTS_END', dts);
# suspend;
# end
# ^
# set term ;^
#
# insert into t_log( required_value, actual_value, elap_ms)
# values(
# upper( '%(required_compression)s' )
# ,upper( rdb$get_context('SYSTEM','WIRE_COMPRESSED') )
# ,datediff( millisecond
# from cast(rdb$get_context('USER_SESSION','DTS_BEG') as timestamp)
# to cast(rdb$get_context('USER_SESSION','DTS_END') as timestamp)
# )
# )
# returning required_value, actual_value, elap_ms
# ;
# commit;
# ''' % dict(globals(), **locals())
# # ( locals() )
#
# f_sql=open( sql_file_name, 'w')
# f_sql.write( sql_text )
# f_sql.close()
#
# #-------------------------
#
# # Call for removing dump from disk:
# prepare_sql_4run( 'false', DB_PATH, None, None )
# prepare_sql_4run( 'true', DB_PATH, None, None )
#
#
# REQUIRED_WIRE_COMPRESSION = 'false'
# # ------------------------------------------------------ ###########
# # Generate SQL script for running when WireCompression = |||FALSE|||
# # ------------------------------------------------------ ###########
# prepare_sql_4run( REQUIRED_WIRE_COMPRESSION, DB_PATH, N_ROWS, F_SQL_NAME )
#
# # ------------------------------------------------------ ###########
# # Update content of firebird.conf with WireCompression = |||FALSE|||
# # ------------------------------------------------------ ###########
# prepare_fb_conf( fb_home, REQUIRED_WIRE_COMPRESSION)
#
#
# # --------------------------------------------------------------------------------------
# # Launch ISQL in separate context of execution with job to obtain data and log duration
# # --------------------------------------------------------------------------------------
#
# fn_log = open(os.devnull, 'w')
# #fn_log = open( os.path.join(context['temp_directory'],'tmp_0733_with_compression.log'), 'w')
# f_isql_obtain_data_err = open( os.path.join(context['temp_directory'],'tmp_0733_obtain_data.err'), 'w')
#
# subprocess.call( [ context['isql_path'], dsn, "-i", F_SQL_NAME ],
# stdout = fn_log,
# stderr = f_isql_obtain_data_err
# )
# fn_log.close()
# f_isql_obtain_data_err.close()
#
# # Call for removing dump from disk:
# #prepare_sql_4run( False, DB_PATH, None, None )
# #prepare_sql_4run( True, DB_PATH, None, None )
#
#
# # Update content of firebird.conf with WireCompression = true
# ##############################################################
#
# REQUIRED_WIRE_COMPRESSION = 'true'
# # ------------------------------------------------------ ###########
# # Generate SQL script for running when WireCompression = ||| TRUE|||
# # ------------------------------------------------------ ###########
# prepare_sql_4run( REQUIRED_WIRE_COMPRESSION, DB_PATH, N_ROWS, F_SQL_NAME )
#
# # ------------------------------------------------------ ###########
# # Update content of firebird.conf with WireCompression = ||| TRUE|||
# # ------------------------------------------------------ ###########
# prepare_fb_conf( fb_home, REQUIRED_WIRE_COMPRESSION)
#
# fn_log = open(os.devnull, 'w')
# #fn_log = open( os.path.join(context['temp_directory'],'tmp_0733_without_compress.log'), 'w')
# f_isql_obtain_data_err = open( os.path.join(context['temp_directory'],'tmp_0733_obtain_data.err'), 'a')
#
# subprocess.call( [ context['isql_path'], dsn, "-i", F_SQL_NAME ],
# stdout = fn_log,
# stderr = f_isql_obtain_data_err
# )
# fn_log.close()
# flush_and_close( f_isql_obtain_data_err )
#
# # Call for removing dump from disk:
# #prepare_sql_4run( REQUIRED_WIRE_COMPRESSION, DB_PATH, None, None )
#
# # RESTORE original config:
# ##########################
# shutil.copy2( fbconf_bak , fb_home+'firebird.conf')
#
# sql='''
# -- select * from t_log;
# -- REQUIRED_VALUE ACTUAL_VALUE ELAP_MS
# -- ============== ============ ============
# -- FALSE FALSE 2187
# -- TRUE TRUE 1782
# set list on;
# select
# result_of_req_compare_to_actual
# --,iif( slowest_with_compression < fastest_without_compression,
# -- 'EXPECTED: compression was FASTER.',
# -- 'POOR. slowest_with_compression=' || slowest_with_compression || ', fastest_without_compression=' || fastest_without_compression
# -- ) as result_of_compression_benchmark
# from (
# select
# min( iif( upper(required_value) is distinct from upper(actual_value)
# ,coalesce(required_value,'<null>') || coalesce(actual_value,'<null>')
# ,'EXPECTED: actual values were equal to required.'
# )
# ) as result_of_req_compare_to_actual
# ,min( iif( upper(required_value) = upper('false'), elap_ms, null ) ) fastest_without_compression
# ,max( iif( upper(required_value) = upper('true'), elap_ms, null ) ) slowest_with_compression
# from t_log
# )
# ;
# set list off;
# --select * from t_log;
#
# '''
# runProgram('isql', [ dsn ], sql)
#
#
# # Additional check: STDERR for ISQL must be EMPTY.
# ##################################################
#
# f_list=(f_isql_obtain_data_err,)
# for i in range(len(f_list)):
# f_name=f_list[i].name
# if os.path.getsize(f_name) > 0:
# with open( f_name,'r') as f:
# for line in f:
# print("Unexpected STDERR, file "+f_name+": "+line)
#
# os.remove(f_isql_obtain_data_err.name)
# os.remove(fbconf_bak)
# os.remove(F_SQL_NAME)
#
#
#---
# Register Firebird server (D:\FB\probes\fid-set-dpb-probe-05x.py)
srv_config_key_value_text = \
f"""
[test_srv_core_0733]
protocol = inet
"""
driver_config.register_server(name = 'test_srv_core_0733', config = srv_config_key_value_text)
db_cfg_object = driver_config.register_database(name = 'test_db_core_0733')
db_cfg_object.database.value = str(act.db.db_path)
# 4debug only, check in trace: db_cfg_object.charset.value = 'win1257'
benchmark_data = defaultdict(list)
for i in range(0, N_MEASURES):
for w_compr in ('true', 'false'):
db_cfg_object.config.value = f"""
WireCompression = {w_compr}
WireCrypt = Disabled
"""
with connect('test_db_core_0733') as con:
with con.cursor() as cur:
cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection')
fb_pid = int(cur.fetchone()[0])
for data_source in ('t_common_text', 't_binary_data'):
cur.execute('select s from ' + data_source)
fb_info_a = psutil.Process(fb_pid).cpu_times()
for r in cur:
# *** FULL FETCH ***
pass
fb_info_b = psutil.Process(fb_pid).cpu_times()
k = ('WireCompression=' + w_compr, 'DataSource='+data_source)
benchmark_data[k] += fb_info_b.user - fb_info_a.user,
compressed_txt = [ v for k,v in benchmark_data.items() if k[0] == 'WireCompression=true' and k[1] == 'DataSource=t_common_text'][0]
compressed_bin = [ v for k,v in benchmark_data.items() if k[0] == 'WireCompression=true' and k[1] == 'DataSource=t_binary_data'][0]
non_comprs_txt = [ v for k,v in benchmark_data.items() if k[0] == 'WireCompression=false' and k[1] == 'DataSource=t_common_text'][0]
non_comprs_bin = [ v for k,v in benchmark_data.items() if k[0] == 'WireCompression=false' and k[1] == 'DataSource=t_binary_data'][0]
cpu_txt_wcompr_ON_vs_OFF = median(compressed_txt) / max(0.00001, median(non_comprs_txt))
cpu_bin_wcompr_ON_vs_OFF = median(compressed_bin) / max(0.00001, median(non_comprs_bin))
cpu_txt2bin_wcompr_OFF = median(non_comprs_txt) / max(0.00001, median(non_comprs_bin))
msg_result1 = 'CPU time ratio when sending %s and WireCompression is ON vs OFF: %s'
what_sent1 = 'COMPRESSABLE TEXTUAL DATA'
all_fine = 1
if cpu_txt_wcompr_ON_vs_OFF > MIN_CPU_RATIO_TXT_WCOMPR_ON_OFF:
print(msg_result1 % (what_sent1, 'EXPECTED') )
else:
print(msg_result1 % (what_sent1, 'UNEXPECTED, less than '+str(MIN_CPU_RATIO_TXT_WCOMPR_ON_OFF)) )
all_fine = 0
what_sent2 = 'INCOMPRESSABLE BINARY DATA'
if cpu_bin_wcompr_ON_vs_OFF > MIN_CPU_RATIO_BIN_WCOMPR_ON_OFF:
print(msg_result1 % (what_sent2, 'EXPECTED') )
else:
print(msg_result1 % (what_sent2, 'UNEXPECTED, less than '+str(MIN_CPU_RATIO_BIN_WCOMPR_ON_OFF)) )
all_fine = 0
msg_result2 = 'CPU time ratio when WireCompression is OFF and sending TEXTUAL vs BINARY data: %s'
if cpu_txt2bin_wcompr_OFF <= MAX_CPU_RATIO_TXT2BIN_WCOMPR_OFF:
print(msg_result2 % ('EXPECTED') )
else:
print(msg_result2 % ('UNEXPECTED, more than ' + str(MAX_CPU_RATIO_TXT2BIN_WCOMPR_OFF)) )
all_fine = 0
if not all_fine:
print('compressed_txt=',compressed_txt,' min=',min(compressed_txt),' max=',max(compressed_txt))
print('compressed_bin=',compressed_bin,' min=',min(compressed_bin),' max=',max(compressed_bin))
print('non_comprs_txt=',non_comprs_txt,' min=',min(non_comprs_txt),' max=',max(non_comprs_txt))
print('non_comprs_bin=',non_comprs_bin,' min=',min(non_comprs_bin),' max=',max(non_comprs_bin))
print('median(compressed_txt) / median(non_comprs_txt) =',1.000 * median(compressed_txt) / max(0.00001, median(non_comprs_txt)) )
print('median(compressed_bin) / median(non_comprs_bin) =',1.000 * median(compressed_bin) / max(0.00001, median(non_comprs_bin)) )
print('median(non_comprs_txt) / median(non_comprs_bin) =',1.000 * median(non_comprs_txt) / max(0.00001, median(non_comprs_bin)) )
expected_stdout = ''
for f in (what_sent1, what_sent2):
expected_stdout += ''.join( (msg_result1 % (f, 'EXPECTED'), '\n') )
expected_stdout += (msg_result2 % 'EXPECTED')
act.expected_stdout = expected_stdout
act.stdout = capsys.readouterr().out
assert act.clean_stdout == act.clean_expected_stdout