In profondità

Il consumo di CPU in Windows NT

Si vuole che il Profiler sia anche in grado di misurare il consumo di CPU di ogni thread Java. Questo obiettivo, forse un po' ambizioso, è stato per ora realizzato sotto Windows NT, ma si spera che in seguito sia disponibile anche sulle altre piattaforme.

Il problema è semplice in linea di principio ma un po' complicato dal punto di vista implementativo. Il codice che segue è presentato per dare una panoramica della metodologia adottata; tutti i dettagli sulle strutture dati si possono trovare nella guida in linea del winSDK (Software Development Kit).

Innanzitutto, non appena un thread nasce occorre identificare il thread nativo su cui si appoggia, basandosi sull'ipotesi che la Microsoft JVM utilizza per l'appunto i thread nativi di Windows. Il codice che segue:

  case JVMPI_EVENT_THREAD_START:
    Stat stat;
    stat.obj_num=0;
    ...
    lock();
  #ifdef WIN32
    //questa è una funzione SDK
    stat.tid=GetCurrentThreadId();
  #endif
    stat.thread=getJavaCurrentThread(ev->env_id);
    thread_table.add(ev->env_id,stat);
    unlock();
    break;
mostra come, oltre al riferimento al thread Java, ora stat contenga anche un ID del thread nativo di Windows. Le misure sulla CPU verranno effettuate solo al momento della lettura dei dati, ma per ora è importante avere un aggancio al thread. Si noti che la mappatura thread_Java-thread_Win32 è univoca e dura per tutto il tempo di vita del thread. Queste ipotesi sono fondamentali per capire la metodologia usata, e potrebbero non essere vere per altre piattaforme.

Il registro di sistema

La tecnica utilizzata per recuperare il consumo di CPU è quella di chiederlo direttamente al registro di sistema. Le primitive sono le stesse utilizzate dal tool di sistema PerformanceMonitor.
I passi da compiere sono tre:

La funzione freeze( ) esegue la prima fase.
  //una variabile globale
  static PPERF_DATA_BLOCK perfdata;

  void freeze()
    {
    perfdata = CollectData("232");
    }


  PPERF_DATA_BLOCK CollectData(LPTSTR query)
    {
    //un puntatore ad un buffer generico
    LPBYTE data;
    //una quasi costante
    DWORD BufferSize=4096;
    //allocazione del buffer
    data= malloc( BufferSize );
    //con questa funzione interrogo il registro di sistema
    //alla chiave HKEY_PERFORMANCE_DATA
    while(RegQueryValueEx( HKEY_PERFORMANCE_DATA, query, NULL, NULL,
                           data, &BufferSize ) == ERROR_MORE_DATA )
      {
      //se il buffer fosse troppo piccolo per contenere
      //i dati, basterà richiedere più memoria
      BufferSize += 4096;
      data= realloc( data, BufferSize );
      }
    //chiudo le comunicazioni con il registro
    RegCloseKey(HKEY_PERFORMANCE_DATA);
    return (PPERF_DATA_BLOCK) data;
    }
Niente paura e andiamo con ordine. La variabile perfdata è un puntatore ad una struttura dati enorme e complicatissima, che serve a contenere una serie di contatori di performance relativi ad alcuni oggetti di Windows. In questo caso gli oggetti sono i thread, che corrispondono alla query "232". Il resto sono esercizi coi puntatori.

Praticamente tutti i dati sono stati già acquisiti. Si tratta ora, per passare alla seconda fase, di sapersi muovere all'interno della struttura perfdata, cosa tutt'altro che semplice e che pertanto si delega direttamente alla lettura del codice sorgente. Quindi illustreremo soltanto la funzione getCpu, che ovviamente andrà chiamata per ogni thread presente nella thread_table con time=getCpu(stat->tid).

  jlong getCpu(DWORD tid)
    {
    PPERF_OBJECT_TYPE obj;
    PPERF_INSTANCE_DEFINITION inst;
    PPERF_COUNTER_DEFINITION cntr[2];
    DWORD index[]={804,6};
    DWORD i;

    //cerco l'oggetto 232 (thread)
    obj = FindObject( perfdata ,232);

    //cerco i contatori 804 (tid) e 6 (cpu)
    FindCounters(obj,index,cntr,2);

    //cerco l'istanza corrispondente all'argomento tid
    for(i=0,inst = FirstInstance( obj );
        i < obj->NumInstances;  
        i++,inst = NextInstance( inst ))
      if(InstanceData(inst,cntr[0])==tid)
        //restituisco il consumo di CPU
        //dividendo per 10000 per ottenere millisecondi
        return InstanceData(inst,cntr[1])/10000;
    }
    return 0;
    }
Infine, nella terza ed ultima fase, si libererà la memoria precedentemente allocata per perfdata.
  void unfreeze()
    {                           
    free(perfdata);
    }
Per qualsiasi altra informazione si rimanda alla guida in linea dell'SDK e di PerformanceMonitor.

precedente