Skip to content

Commit

Permalink
[swig]: Fix swig template memory leak on issue 17025 (sonic-net#876)
Browse files Browse the repository at this point in the history
Fix the issue sonic-net/sonic-buildimage#17025 about Redis set activity

Description
The issue reports a memory leak on the Redis set operations

Reason
Didn't decrease the reference count after PySequence_GetItem
Use the inappropriate Swig API and didn't release the string of SWIG_AsPtr_std_string
Fix:
Refer PR: Fix swig template memory leak sonic-net#859 from @praveenraja1
Replace the API SWIG_AsPtr_std_string to SWIG_AsVal_std_string
Add unit test
To monitor there is no dramatic memory increment after a huge amount of Redis set operations.

Signed-off-by: Ze Gan <zegan@microsoft.com>
  • Loading branch information
Pterosaur committed May 29, 2024
1 parent 8dc6218 commit a66dcbd
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .azure-pipelines/build-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
displayName: "Install gcovr 5.2 (for --exclude-throw-branches support)"
- script: |
set -ex
sudo pip install Pympler==0.8
sudo pip install Pympler==0.8 pytest psutil
sudo apt-get install -y redis-server
sudo sed -i 's/notify-keyspace-events ""/notify-keyspace-events AKE/' /etc/redis/redis.conf
sudo sed -ri 's/^# unixsocket/unixsocket/' /etc/redis/redis.conf
Expand Down
39 changes: 25 additions & 14 deletions pyext/swsscommon.i
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
#include "zmqclient.h"
#include "zmqconsumerstatetable.h"
#include "zmqproducerstatetable.h"
#include <memory>
#include <functional>
%}

%include <std_string.i>
Expand Down Expand Up @@ -152,31 +154,36 @@
SWIG_Python_AppendOutput($result, temp);
}

%typemap(in, fragment="SWIG_AsPtr_std_string")
%typemap(in, fragment="SWIG_AsVal_std_string")
const std::vector<std::pair< std::string,std::string >,std::allocator< std::pair< std::string,std::string > > > &
(std::vector< std::pair< std::string,std::string >,std::allocator< std::pair< std::string,std::string > > > temp,
int res) {
res = SWIG_OK;
for (int i = 0; i < PySequence_Length($input); ++i) {
temp.push_back(std::pair< std::string,std::string >());
PyObject *item = PySequence_GetItem($input, i);
if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
std::unique_ptr<PyObject, std::function<void(PyObject *)> > item(
PySequence_GetItem($input, i),
[](PyObject *ptr){
Py_DECREF(ptr);
});
if (!PyTuple_Check(item.get()) || PyTuple_Size(item.get()) != 2) {
SWIG_fail;
}
PyObject *key = PyTuple_GetItem(item, 0);
PyObject *value = PyTuple_GetItem(item, 1);
std::string *ptr = (std::string *)0;
PyObject *key = PyTuple_GetItem(item.get(), 0);
PyObject *value = PyTuple_GetItem(item.get(), 1);
std::string str;

if (PyBytes_Check(key)) {
temp.back().first.assign(PyBytes_AsString(key), PyBytes_Size(key));
} else if (SWIG_AsPtr_std_string(key, &ptr)) {
temp.back().first = *ptr;
} else if (SWIG_AsVal_std_string(key, &str) != SWIG_ERROR) {
temp.back().first = str;
} else {
SWIG_fail;
}
if (PyBytes_Check(value)) {
temp.back().second.assign(PyBytes_AsString(value), PyBytes_Size(value));
} else if (SWIG_AsPtr_std_string(value, &ptr)) {
temp.back().second = *ptr;
} else if (SWIG_AsVal_std_string(value, &str) != SWIG_ERROR) {
temp.back().second = str;
} else {
SWIG_fail;
}
Expand All @@ -187,13 +194,17 @@
%typemap(typecheck) const std::vector< std::pair< std::string,std::string >,std::allocator< std::pair< std::string,std::string > > > &{
$1 = 1;
for (int i = 0; i < PySequence_Length($input); ++i) {
PyObject *item = PySequence_GetItem($input, i);
if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
std::unique_ptr<PyObject, std::function<void(PyObject *)> > item(
PySequence_GetItem($input, i),
[](PyObject *ptr){
Py_DECREF(ptr);
});
if (!PyTuple_Check(item.get()) || PyTuple_Size(item.get()) != 2) {
$1 = 0;
break;
}
PyObject *key = PyTuple_GetItem(item, 0);
PyObject *value = PyTuple_GetItem(item, 1);
PyObject *key = PyTuple_GetItem(item.get(), 0);
PyObject *value = PyTuple_GetItem(item.get(), 1);
if (!PyBytes_Check(key)
&& !PyUnicode_Check(key)
&& !PyString_Check(key)
Expand Down
70 changes: 69 additions & 1 deletion tests/test_redis_ut.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import time
import psutil
import pytest
import multiprocessing
from threading import Thread
Expand Down Expand Up @@ -801,4 +802,71 @@ def test_ConfigDBConnector():
allconfig["PORT_TABLE"] = None
config_db.mod_config(allconfig)
allconfig = config_db.get_config()
assert len(allconfig) == 0
assert len(allconfig) == 0


@mock.patch("swsscommon.swsscommon.ConfigDBConnector.close")
def test_ConfigDBConnector_with_statement(self):
# test ConfigDBConnector support 'with' statement
with ConfigDBConnector() as config_db:
assert config_db.db_name == ""
assert config_db.TABLE_NAME_SEPARATOR == "|"
config_db.connect(wait_for_init=False)
assert config_db.db_name == "CONFIG_DB"
assert config_db.TABLE_NAME_SEPARATOR == "|"
config_db.get_redis_client(config_db.CONFIG_DB).flushdb()
config_db.set_entry("TEST_PORT", "Ethernet111", {"alias": "etp1x"})
allconfig = config_db.get_config()
assert allconfig["TEST_PORT"]["Ethernet111"]["alias"] == "etp1x"

# check close() method called by with statement
ConfigDBConnector.close.assert_called_once_with()


def test_SmartSwitchDBConnector():
test_dir = os.path.dirname(os.path.abspath(__file__))
global_db_config = os.path.join(test_dir, 'redis_multi_db_ut_config', 'database_global.json')
SonicDBConfig.load_sonic_global_db_config(global_db_config)
global_db_config_json = json.load(open(global_db_config))
db_key = SonicDBKey()
db_key.containerName = "dpu0"
db = swsscommon.DBConnector("DPU_APPL_DB", 0, True, db_key)
tbl = swsscommon.Table(db, "DASH_ENI_TABLE")
fvs = swsscommon.FieldValuePairs([('dashfield1','dashvalue1'), ('dashfield2', 'dashvalue2')])
tbl.set("dputest1", fvs)
tbl.set("dputest2", fvs)
keys = tbl.getKeys()
assert len(keys) == 2
assert "dputest1" in keys
assert "dputest2" in keys
assert tbl.get("dputest1")[1][0] == ("dashfield1", "dashvalue1")
assert tbl.get("dputest2")[1][1] == ("dashfield2", "dashvalue2")
assert len(SonicDBConfig.getDbKeys()) == len(global_db_config_json["INCLUDES"])


def test_TableSetBinary():
app_db = swsscommon.DBConnector("APPL_DB", 0, True)
t = swsscommon.Table(app_db, "TABLE")
buff = b""
for i in range(0, 256):
buff += bytes([i])
buff = buff.decode('latin-1')
fvs = swsscommon.FieldValuePairs([("binary", buff)])
t.set("binary", fvs)
(status, fvs) = t.get("binary")
assert status == True
assert fvs[0][1] == buff


def test_TableOpsMemoryLeak():
OP_COUNT = 50000
app_db = swsscommon.DBConnector("APPL_DB", 0, True)
t = swsscommon.Table(app_db, "TABLE")
long_data = "x" * 100
fvs = swsscommon.FieldValuePairs([(long_data, long_data)])
rss = psutil.Process(os.getpid()).memory_info().rss
for _ in range(OP_COUNT):
t.set("long_data", fvs)
t.get("long_data")
assert psutil.Process(os.getpid()).memory_info().rss - rss < OP_COUNT

0 comments on commit a66dcbd

Please sign in to comment.