Проблемы с ICU. Подход #2

Салют всем.

Хочется двигаться вперед — к программированию по-настоящему интересных вещей, однако грехи старые проблемы тяготят сознание. В последнем выпуске провайдера (v3.9) многие из них были закрыты. Даже хочется думать, что практически все. Кроме утечек ресурсов, связанных с ICU. Я о них тут уже писал. Полгода назад. А сейчас вот предпринял повторную попытку разобраться в причинах. Надеясь, что 5-ти недельный отпуск достаточно растормозил мозги для решения этой задачи. Итак.

Описание проблемы.
Утечка дескрипторов в тестовом процессе (в котором работает провайдер).

Когда происходит
1. Многопоточная работа.
2. Есть выгрузка ICU из тестового процесса.

Кто течет?
Судя по всему — не освобождаются критические секции, которые ICU использует для собственной защиты в многопоточной среде.

Чуть меньше двух лет назад, наступив на первые грабли с ICU (icuuc30.dll), я модифицировал эту DLL — добавил DllMain с инициализацией и деинициализацией:

BOOL WINAPI DllMain(HINSTANCE const /*hInstance*/,
                    DWORD     const dwReason,
                    LPVOID    const /*lpReserved*/)
{
 BOOL bResult=TRUE;

 try
 {
  switch(dwReason)
  {
   case DLL_PROCESS_ATTACH:
   {
    umtx_init(NULL);
    break;
   }//DLL_PROCESS_ATTACH

   case DLL_PROCESS_DETACH:
   {
    //u_cleanup();
    umtx_cleanup();

    break;
   }//DLL_PROCESS_DETACH
  }//switch
 }
 catch(...)
 {
  assert(false && "ICU DllMain");

  bResult=FALSE;
 }

 return bResult;
}//DllMain

Изначально здесь главным был вызов «umtx_init(NULL)», который монопольно инициализировал объекты синхронизации.

Вызов «umtx_cleanup()» (согласно документации для ICU) должен освободить все критические секции. И тем самым освободить дескрипторы, которые к ним привязаны. Однако дескрипторы текли и долгое время была мысль — «где-то в недрах icuuc30.dll есть еще объекты, которые жрут дескрипторы».

На днях эта мысль вместе с этой утечкой меня (в очередной раз) задрали утомили и я прошелся отладчиком по всему коду ICU, который вызывается из провайдера. Обнаружил, что в глобальных переменных сохраняются указатели на динамически выделяемую память. Ага. Значит все таки надо вызывать «u_cleanup()» — в ней есть и вызов «umtx_cleanup()» и память освобождается. Не помогло — дескрипторы все еще текут.

Вот ёлкин дрын. Полез в реализацию umtx_cleanup(). Вот её оригинальный код:

U_CFUNC UBool umtx_cleanup(void) {
    umtx_destroy(NULL);
    pMutexInitFn    = NULL;
    pMutexDestroyFn = NULL;
    pMutexLockFn    = NULL;
    pMutexUnlockFn  = NULL;
    gMutexContext   = NULL;
    gGlobalMutex    = NULL;
    pIncFn          = NULL;
    pDecFn          = NULL;
    gIncDecMutex    = NULL;

#if (ICU_USE_THREADS == 1)
    if (gMutexPoolInitialized) {
        int i;
        for (i=0; i<MAX_MUTEXES; i++) {
            if (gMutexesInUse[i]) {
#if defined (WIN32)
                DeleteCriticalSection(&gMutexes[i]);
#elif defined (POSIX) && ! defined (SOLARIS_MT)
                pthread_mutex_destroy(&gMutexes[i]);
#elif defined (SOLARIS_MT)
				mutex_destroy(&gMutexes[i]);
#endif
                gMutexesInUse[i] = 0;
            }
        }
    }
    gMutexPoolInitialized = FALSE;
#endif

    return TRUE;
}

Тупо на него посмотрел.

Потом посмотрел на initGlobalMutex (в этом же C-файле), в котором gMutexes инициализируются.

Потом — на функции umtx_init/umtx_destroy, в которых идет модификация gMutexesInUse.

И понял.
gMutexes — это пул готовых критических секций.
gMutexesInUse[i] — это флаги занятости элементов из gMutexes. Когда элемент освобождается, сюда пишется 0, а сама критическая секция не освобождается.

То есть вот это вот «if (gMutexesInUse[i])» в umtx_cleanup — вообще не в тему. И разрушать критическую секцию надо по-любому. А если критическая секция «InUse», то тот кто не освободил мьютекс — ССЗБ.

Вообщем, сделал вот так:

U_CFUNC UBool umtx_cleanup(void) {
    umtx_destroy(NULL);
    pMutexInitFn    = NULL;
    pMutexDestroyFn = NULL;
    pMutexLockFn    = NULL;
    pMutexUnlockFn  = NULL;
    gMutexContext   = NULL;
    gGlobalMutex    = NULL;
    pIncFn          = NULL;
    pDecFn          = NULL;
    gIncDecMutex    = NULL;

#if (ICU_USE_THREADS == 1)
    if (gMutexPoolInitialized) {
        int i;
        for (i=0; i<MAX_MUTEXES; i++) {
            U_ASSERT(gMutexesInUse[i] == 0);

            /*if (gMutexesInUse[i])*/ {
#if defined (WIN32)
                DeleteCriticalSection(&gMutexes[i]);
#elif defined (POSIX) && ! defined (SOLARIS_MT)
                pthread_mutex_destroy(&gMutexes[i]);
#elif defined (SOLARIS_MT)
				mutex_destroy(&gMutexes[i]);
#endif
                gMutexesInUse[i] = 0;
            }
        }
    }
    gMutexPoolInitialized = FALSE;
#endif

    return TRUE;
}

DllMain выглядит вот так

BOOL WINAPI DllMain(HINSTANCE const /*hInstance*/,
                    DWORD     const dwReason,
                    LPVOID    const /*lpReserved*/)
{
 BOOL bResult=TRUE;

 try
 {
  switch(dwReason)
  {
   case DLL_PROCESS_ATTACH:
   {
    umtx_init(NULL);
    break;
   }//DLL_PROCESS_ATTACH

   case DLL_PROCESS_DETACH:
   {
    u_cleanup();
    break;
   }//DLL_PROCESS_DETACH
  }//switch
 }
 catch(...)
 {
  assert(false && "ICU DllMain");

  bResult=FALSE;
 }

 return bResult;
}//DllMain

И, кажись — помогло!

Многочасовой многопоточный прогон тестов для ICU-шных кодовых страниц GB18030 и CP943C на финише выдал такие состояния тестового и серверного процесса (FB SuperClassic x64):


Process Explorer: fb_inet_server.exe Image.
Сведения о серверном процессе.


Process Explorer: fb_inet_server.exe Perfomance.
Ресурсы серверного процесса.


Process Explorer: ibp_oledb_test_vc10_x64_Release.exe Image.
Сведения о тестовом процессе.


Process Explorer: ibp_oledb_test_vc10_x64_Release.exe Perfomance.
Ресурсы тестового процесса.

Как видно из последней картинки — после завершения всех тестов и выгрузки всех модулей (провайдер, fbclient.dll, icuuc30.dll) и деинициализации COM у тестового процесса осталось 95 дескрипторов. Это типа реально круто. Потому что раньше было — больше тысячи.

По памяти (Private Bytes) все еще остаются вопросы. Хотя у сервера и тестового процесса — эти значения соизмеримы. Но это я еще покопаюсь в ICU.

Главное — перестали течь дескрипторы.

А теперь надо
1. взять оригинальный встроенный сервер FB c его оригинальным ICU
2. провайдер+модифицированное ICU
И прогнать эти же тесты в многопоточном режиме с периодической выгрузкой встроенного сервера из памяти процесса. Сервер — будет работать со своим ICU, провайдер — со своим.

Хотя оно и так понятно, что будут течь и дескрипторы и память. Но надо убедиться 🙂

2 комментария

Kovalenko  on 27 августа, 2011

Протестировать ICU встроенного сервера — ха.

Да он сам по себе валится если его загружать/выгружать.

Почему он это делает — пока не знаю. И, честно говоря, в данный конкретный момент даже не тянет узнавать — зачем портить себе выходные? 🙂


Кстати, вчера исполнился ровно один год с момента релиза третьей версии провайдера.

Kovalenko  on 29 августа, 2011

Раз со встроенным ничего не вышло — на выходных тестировал обычный нормальный суперклассик. Общая продолжительность прогона 41 час.

С дескрипторами все тип-топ. И это пока все еще радует.

Памяти у сервера утекло ~890MB. В тестовом процессе: ~1090MB.

Сдается мне — что-то в этом ICU3 еще есть 🙂

Leave a Comment