/****************************************************************************** * * Name: skdim.c * Project: GEnesis, PCI Gigabit Ethernet Adapter * Version: $Revision: 1.5 $ * Date: $Date: 2003/11/28 12:55:40 $ * Purpose: All functions to maintain interrupt moderation * ******************************************************************************/ /****************************************************************************** * * (C)Copyright 1998-2002 SysKonnect GmbH. * (C)Copyright 2002-2003 Marvell. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * The information in this file is provided "AS IS" without warranty. * ******************************************************************************/ /****************************************************************************** * * Description: * * This module is intended to manage the dynamic interrupt moderation on both * GEnesis and Yukon adapters. * * Include File Hierarchy: * * "skdrv1st.h" * "skdrv2nd.h" * ******************************************************************************/ #ifndef lint static const char SysKonnectFileId[] = "@(#) $Id: skdim.c,v 1.5 2003/11/28 12:55:40 rroesler Exp $ (C) SysKonnect."; #endif #define __SKADDR_C #ifdef __cplusplus #error C++ is not yet supported. extern "C" { #endif /******************************************************************************* ** ** Includes ** *******************************************************************************/ #ifndef __INC_SKDRV1ST_H #include "h/skdrv1st.h" #endif #ifndef __INC_SKDRV2ND_H #include "h/skdrv2nd.h" #endif #include /******************************************************************************* ** ** Defines ** *******************************************************************************/ /******************************************************************************* ** ** Typedefs ** *******************************************************************************/ /******************************************************************************* ** ** Local function prototypes ** *******************************************************************************/ static unsigned int GetCurrentSystemLoad(SK_AC *pAC); static SK_U64 GetIsrCalls(SK_AC *pAC); static SK_BOOL IsIntModEnabled(SK_AC *pAC); static void SetCurrIntCtr(SK_AC *pAC); static void EnableIntMod(SK_AC *pAC); static void DisableIntMod(SK_AC *pAC); static void ResizeDimTimerDuration(SK_AC *pAC); static void DisplaySelectedModerationType(SK_AC *pAC); static void DisplaySelectedModerationMask(SK_AC *pAC); static void DisplayDescrRatio(SK_AC *pAC); /******************************************************************************* ** ** Global variables ** *******************************************************************************/ /******************************************************************************* ** ** Local variables ** *******************************************************************************/ /******************************************************************************* ** ** Global functions ** *******************************************************************************/ /******************************************************************************* ** Function : SkDimModerate ** Description : Called in every ISR to check if moderation is to be applied ** or not for the current number of interrupts ** Programmer : Ralph Roesler ** Last Modified: 22-mar-03 ** Returns : void (!) ** Notes : - *******************************************************************************/ void SkDimModerate(SK_AC *pAC) { unsigned int CurrSysLoad = 0; /* expressed in percent */ unsigned int LoadIncrease = 0; /* expressed in percent */ SK_U64 ThresholdInts = 0; SK_U64 IsrCallsPerSec = 0; #define M_DIMINFO pAC->DynIrqModInfo if (!IsIntModEnabled(pAC)) { if (M_DIMINFO.IntModTypeSelect == C_INT_MOD_DYNAMIC) { CurrSysLoad = GetCurrentSystemLoad(pAC); if (CurrSysLoad > 75) { /* ** More than 75% total system load! Enable the moderation ** to shield the system against too many interrupts. */ EnableIntMod(pAC); } else if (CurrSysLoad > M_DIMINFO.PrevSysLoad) { LoadIncrease = (CurrSysLoad - M_DIMINFO.PrevSysLoad); if (LoadIncrease > ((M_DIMINFO.PrevSysLoad * C_INT_MOD_ENABLE_PERCENTAGE) / 100)) { if (CurrSysLoad > 10) { /* ** More than 50% increase with respect to the ** previous load of the system. Most likely this ** is due to our ISR-proc... */ EnableIntMod(pAC); } } } else { /* ** Neither too much system load at all nor too much increase ** with respect to the previous system load. Hence, we can leave ** the ISR-handling like it is without enabling moderation. */ } M_DIMINFO.PrevSysLoad = CurrSysLoad; } } else { if (M_DIMINFO.IntModTypeSelect == C_INT_MOD_DYNAMIC) { ThresholdInts = ((M_DIMINFO.MaxModIntsPerSec * C_INT_MOD_DISABLE_PERCENTAGE) / 100); IsrCallsPerSec = GetIsrCalls(pAC); if (IsrCallsPerSec <= ThresholdInts) { /* ** The number of interrupts within the last second is ** lower than the disable_percentage of the desried ** maxrate. Therefore we can disable the moderation. */ DisableIntMod(pAC); M_DIMINFO.MaxModIntsPerSec = (M_DIMINFO.MaxModIntsPerSecUpperLimit + M_DIMINFO.MaxModIntsPerSecLowerLimit) / 2; } else { /* ** The number of interrupts per sec is the same as expected. ** Evalulate the descriptor-ratio. If it has changed, a resize ** in the moderation timer might be useful */ if (M_DIMINFO.AutoSizing) { ResizeDimTimerDuration(pAC); } } } } /* ** Some information to the log... */ if (M_DIMINFO.DisplayStats) { DisplaySelectedModerationType(pAC); DisplaySelectedModerationMask(pAC); DisplayDescrRatio(pAC); } M_DIMINFO.NbrProcessedDescr = 0; SetCurrIntCtr(pAC); } /******************************************************************************* ** Function : SkDimStartModerationTimer ** Description : Starts the audit-timer for the dynamic interrupt moderation ** Programmer : Ralph Roesler ** Last Modified: 22-mar-03 ** Returns : void (!) ** Notes : - *******************************************************************************/ void SkDimStartModerationTimer(SK_AC *pAC) { SK_EVPARA EventParam; /* Event struct for timer event */ SK_MEMSET((char *) &EventParam, 0, sizeof(EventParam)); EventParam.Para32[0] = SK_DRV_MODERATION_TIMER; SkTimerStart(pAC, pAC->IoBase, &pAC->DynIrqModInfo.ModTimer, SK_DRV_MODERATION_TIMER_LENGTH, SKGE_DRV, SK_DRV_TIMER, EventParam); } /******************************************************************************* ** Function : SkDimEnableModerationIfNeeded ** Description : Either enables or disables moderation ** Programmer : Ralph Roesler ** Last Modified: 22-mar-03 ** Returns : void (!) ** Notes : This function is called when a particular adapter is opened ** There is no Disable function, because when all interrupts ** might be disable, the moderation timer has no meaning at all ******************************************************************************/ void SkDimEnableModerationIfNeeded(SK_AC *pAC) { if (M_DIMINFO.IntModTypeSelect == C_INT_MOD_STATIC) { EnableIntMod(pAC); /* notification print in this function */ } else if (M_DIMINFO.IntModTypeSelect == C_INT_MOD_DYNAMIC) { SkDimStartModerationTimer(pAC); if (M_DIMINFO.DisplayStats) { printk("Dynamic moderation has been enabled\n"); } } else { if (M_DIMINFO.DisplayStats) { printk("No moderation has been enabled\n"); } } } /******************************************************************************* ** Function : SkDimDisplayModerationSettings ** Description : Displays the current settings regarding interrupt moderation ** Programmer : Ralph Roesler ** Last Modified: 22-mar-03 ** Returns : void (!) ** Notes : - *******************************************************************************/ void SkDimDisplayModerationSettings(SK_AC *pAC) { DisplaySelectedModerationType(pAC); DisplaySelectedModerationMask(pAC); } /******************************************************************************* ** ** Local functions ** *******************************************************************************/ /******************************************************************************* ** Function : GetCurrentSystemLoad ** Description : Retrieves the current system load of the system. This load ** is evaluated for all processors within the system. ** Programmer : Ralph Roesler ** Last Modified: 22-mar-03 ** Returns : unsigned int: load expressed in percentage ** Notes : The possible range being returned is from 0 up to 100. ** Whereas 0 means 'no load at all' and 100 'system fully loaded' ** It is impossible to determine what actually causes the system ** to be in 100%, but maybe that is due to too much interrupts. *******************************************************************************/ static unsigned int GetCurrentSystemLoad(SK_AC *pAC) { unsigned long jif = jiffies; unsigned int UserTime = 0; unsigned int SystemTime = 0; unsigned int NiceTime = 0; unsigned int IdleTime = 0; unsigned int TotalTime = 0; unsigned int UsedTime = 0; unsigned int SystemLoad = 0; /* unsigned int NbrCpu = 0; */ /* ** The following lines have been commented out, because ** from kernel 2.5.44 onwards, the kernel-owned structure ** ** struct kernel_stat kstat ** ** is not marked as an exported symbol in the file ** ** kernel/ksyms.c ** ** As a consequence, using this driver as KLM is not possible ** and any access of the structure kernel_stat via the ** dedicated macros kstat_cpu(i).cpustat.xxx is to be avoided. ** ** The kstat-information might be added again in future ** versions of the 2.5.xx kernel, but for the time being, ** number of interrupts will serve as indication how much ** load we currently have... ** ** for (NbrCpu = 0; NbrCpu < num_online_cpus(); NbrCpu++) { ** UserTime = UserTime + kstat_cpu(NbrCpu).cpustat.user; ** NiceTime = NiceTime + kstat_cpu(NbrCpu).cpustat.nice; ** SystemTime = SystemTime + kstat_cpu(NbrCpu).cpustat.system; ** } */ SK_U64 ThresholdInts = 0; SK_U64 IsrCallsPerSec = 0; ThresholdInts = ((M_DIMINFO.MaxModIntsPerSec * C_INT_MOD_ENABLE_PERCENTAGE) + 100); IsrCallsPerSec = GetIsrCalls(pAC); if (IsrCallsPerSec >= ThresholdInts) { /* ** We do not know how much the real CPU-load is! ** Return 80% as a default in order to activate DIM */ SystemLoad = 80; return (SystemLoad); } UsedTime = UserTime + NiceTime + SystemTime; IdleTime = jif * num_online_cpus() - UsedTime; TotalTime = UsedTime + IdleTime; SystemLoad = ( 100 * (UsedTime - M_DIMINFO.PrevUsedTime) ) / (TotalTime - M_DIMINFO.PrevTotalTime); if (M_DIMINFO.DisplayStats) { printk("Current system load is: %u\n", SystemLoad); } M_DIMINFO.PrevTotalTime = TotalTime; M_DIMINFO.PrevUsedTime = UsedTime; return (SystemLoad); } /******************************************************************************* ** Function : GetIsrCalls ** Description : Depending on the selected moderation mask, this function will ** return the number of interrupts handled in the previous time- ** frame. This evaluated number is based on the current number ** of interrupts stored in PNMI-context and the previous stored ** interrupts. ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : int: the number of interrupts being executed in the last ** timeframe ** Notes : It makes only sense to call this function, when dynamic ** interrupt moderation is applied *******************************************************************************/ static SK_U64 GetIsrCalls(SK_AC *pAC) { SK_U64 RxPort0IntDiff = 0; SK_U64 RxPort1IntDiff = 0; SK_U64 TxPort0IntDiff = 0; SK_U64 TxPort1IntDiff = 0; if (pAC->DynIrqModInfo.MaskIrqModeration == IRQ_MASK_TX_ONLY) { if (pAC->GIni.GIMacsFound == 2) { TxPort1IntDiff = pAC->Pnmi.Port[1].TxIntrCts - pAC->DynIrqModInfo.PrevPort1TxIntrCts; } TxPort0IntDiff = pAC->Pnmi.Port[0].TxIntrCts - pAC->DynIrqModInfo.PrevPort0TxIntrCts; } else if (pAC->DynIrqModInfo.MaskIrqModeration == IRQ_MASK_RX_ONLY) { if (pAC->GIni.GIMacsFound == 2) { RxPort1IntDiff = pAC->Pnmi.Port[1].RxIntrCts - pAC->DynIrqModInfo.PrevPort1RxIntrCts; } RxPort0IntDiff = pAC->Pnmi.Port[0].RxIntrCts - pAC->DynIrqModInfo.PrevPort0RxIntrCts; } else { if (pAC->GIni.GIMacsFound == 2) { RxPort1IntDiff = pAC->Pnmi.Port[1].RxIntrCts - pAC->DynIrqModInfo.PrevPort1RxIntrCts; TxPort1IntDiff = pAC->Pnmi.Port[1].TxIntrCts - pAC->DynIrqModInfo.PrevPort1TxIntrCts; } RxPort0IntDiff = pAC->Pnmi.Port[0].RxIntrCts - pAC->DynIrqModInfo.PrevPort0RxIntrCts; TxPort0IntDiff = pAC->Pnmi.Port[0].TxIntrCts - pAC->DynIrqModInfo.PrevPort0TxIntrCts; } return (RxPort0IntDiff + RxPort1IntDiff + TxPort0IntDiff + TxPort1IntDiff); } /******************************************************************************* ** Function : GetRxCalls ** Description : This function will return the number of times a receive inter- ** rupt was processed. This is needed to evaluate any resizing ** factor. ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : SK_U64: the number of RX-ints being processed ** Notes : It makes only sense to call this function, when dynamic ** interrupt moderation is applied *******************************************************************************/ static SK_U64 GetRxCalls(SK_AC *pAC) { SK_U64 RxPort0IntDiff = 0; SK_U64 RxPort1IntDiff = 0; if (pAC->GIni.GIMacsFound == 2) { RxPort1IntDiff = pAC->Pnmi.Port[1].RxIntrCts - pAC->DynIrqModInfo.PrevPort1RxIntrCts; } RxPort0IntDiff = pAC->Pnmi.Port[0].RxIntrCts - pAC->DynIrqModInfo.PrevPort0RxIntrCts; return (RxPort0IntDiff + RxPort1IntDiff); } /******************************************************************************* ** Function : SetCurrIntCtr ** Description : Will store the current number orf occured interrupts in the ** adapter context. This is needed to evaluated the number of ** interrupts within a current timeframe. ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : void (!) ** Notes : - *******************************************************************************/ static void SetCurrIntCtr(SK_AC *pAC) { if (pAC->GIni.GIMacsFound == 2) { pAC->DynIrqModInfo.PrevPort1RxIntrCts = pAC->Pnmi.Port[1].RxIntrCts; pAC->DynIrqModInfo.PrevPort1TxIntrCts = pAC->Pnmi.Port[1].TxIntrCts; } pAC->DynIrqModInfo.PrevPort0RxIntrCts = pAC->Pnmi.Port[0].RxIntrCts; pAC->DynIrqModInfo.PrevPort0TxIntrCts = pAC->Pnmi.Port[0].TxIntrCts; } /******************************************************************************* ** Function : IsIntModEnabled() ** Description : Retrieves the current value of the interrupts moderation ** command register. Its content determines whether any ** moderation is running or not. ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : SK_TRUE : if mod timer running ** SK_FALSE : if no moderation is being performed ** Notes : - *******************************************************************************/ static SK_BOOL IsIntModEnabled(SK_AC *pAC) { unsigned long CtrCmd; SK_IN32(pAC->IoBase, B2_IRQM_CTRL, &CtrCmd); if ((CtrCmd & TIM_START) == TIM_START) { return SK_TRUE; } else { return SK_FALSE; } } /******************************************************************************* ** Function : EnableIntMod() ** Description : Enables the interrupt moderation using the values stored in ** in the pAC->DynIntMod data structure ** Programmer : Ralph Roesler ** Last Modified: 22-mar-03 ** Returns : - ** Notes : - *******************************************************************************/ static void EnableIntMod(SK_AC *pAC) { unsigned long ModBase; if (pAC->GIni.GIChipId == CHIP_ID_GENESIS) { ModBase = C_CLK_FREQ_GENESIS / pAC->DynIrqModInfo.MaxModIntsPerSec; } else { ModBase = C_CLK_FREQ_YUKON / pAC->DynIrqModInfo.MaxModIntsPerSec; } SK_OUT32(pAC->IoBase, B2_IRQM_INI, ModBase); SK_OUT32(pAC->IoBase, B2_IRQM_MSK, pAC->DynIrqModInfo.MaskIrqModeration); SK_OUT32(pAC->IoBase, B2_IRQM_CTRL, TIM_START); if (M_DIMINFO.DisplayStats) { printk("Enabled interrupt moderation (%i ints/sec)\n", M_DIMINFO.MaxModIntsPerSec); } } /******************************************************************************* ** Function : DisableIntMod() ** Description : Disables the interrupt moderation independent of what inter- ** rupts are running or not ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : - ** Notes : - *******************************************************************************/ static void DisableIntMod(SK_AC *pAC) { SK_OUT32(pAC->IoBase, B2_IRQM_CTRL, TIM_STOP); if (M_DIMINFO.DisplayStats) { printk("Disabled interrupt moderation\n"); } } /******************************************************************************* ** Function : ResizeDimTimerDuration(); ** Description : Checks the current used descriptor ratio and resizes the ** duration timer (longer/smaller) if possible. ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : - ** Notes : There are both maximum and minimum timer duration value. ** This function assumes that interrupt moderation is already ** enabled! *******************************************************************************/ static void ResizeDimTimerDuration(SK_AC *pAC) { SK_BOOL IncreaseTimerDuration; int TotalMaxNbrDescr; int UsedDescrRatio; int RatioDiffAbs; int RatioDiffRel; int NewMaxModIntsPerSec; int ModAdjValue; long ModBase; /* ** Check first if we are allowed to perform any modification */ if (IsIntModEnabled(pAC)) { if (M_DIMINFO.IntModTypeSelect != C_INT_MOD_DYNAMIC) { return; } else { if (M_DIMINFO.ModJustEnabled) { M_DIMINFO.ModJustEnabled = SK_FALSE; return; } } } /* ** If we got until here, we have to evaluate the amount of the ** descriptor ratio change... */ TotalMaxNbrDescr = pAC->RxDescrPerRing * GetRxCalls(pAC); UsedDescrRatio = (M_DIMINFO.NbrProcessedDescr * 100) / TotalMaxNbrDescr; if (UsedDescrRatio > M_DIMINFO.PrevUsedDescrRatio) { RatioDiffAbs = (UsedDescrRatio - M_DIMINFO.PrevUsedDescrRatio); RatioDiffRel = (RatioDiffAbs * 100) / UsedDescrRatio; M_DIMINFO.PrevUsedDescrRatio = UsedDescrRatio; IncreaseTimerDuration = SK_FALSE; /* in other words: DECREASE */ } else if (UsedDescrRatio < M_DIMINFO.PrevUsedDescrRatio) { RatioDiffAbs = (M_DIMINFO.PrevUsedDescrRatio - UsedDescrRatio); RatioDiffRel = (RatioDiffAbs * 100) / M_DIMINFO.PrevUsedDescrRatio; M_DIMINFO.PrevUsedDescrRatio = UsedDescrRatio; IncreaseTimerDuration = SK_TRUE; /* in other words: INCREASE */ } else { RatioDiffAbs = (M_DIMINFO.PrevUsedDescrRatio - UsedDescrRatio); RatioDiffRel = (RatioDiffAbs * 100) / M_DIMINFO.PrevUsedDescrRatio; M_DIMINFO.PrevUsedDescrRatio = UsedDescrRatio; IncreaseTimerDuration = SK_TRUE; /* in other words: INCREASE */ } /* ** Now we can determine the change in percent */ if ((RatioDiffRel >= 0) && (RatioDiffRel <= 5) ) { ModAdjValue = 1; /* 1% change - maybe some other value in future */ } else if ((RatioDiffRel > 5) && (RatioDiffRel <= 10) ) { ModAdjValue = 1; /* 1% change - maybe some other value in future */ } else if ((RatioDiffRel > 10) && (RatioDiffRel <= 15) ) { ModAdjValue = 1; /* 1% change - maybe some other value in future */ } else { ModAdjValue = 1; /* 1% change - maybe some other value in future */ } if (IncreaseTimerDuration) { NewMaxModIntsPerSec = M_DIMINFO.MaxModIntsPerSec + (M_DIMINFO.MaxModIntsPerSec * ModAdjValue) / 100; } else { NewMaxModIntsPerSec = M_DIMINFO.MaxModIntsPerSec - (M_DIMINFO.MaxModIntsPerSec * ModAdjValue) / 100; } /* ** Check if we exceed boundaries... */ if ( (NewMaxModIntsPerSec > M_DIMINFO.MaxModIntsPerSecUpperLimit) || (NewMaxModIntsPerSec < M_DIMINFO.MaxModIntsPerSecLowerLimit)) { if (M_DIMINFO.DisplayStats) { printk("Cannot change ModTim from %i to %i ints/sec\n", M_DIMINFO.MaxModIntsPerSec, NewMaxModIntsPerSec); } return; } else { if (M_DIMINFO.DisplayStats) { printk("Resized ModTim from %i to %i ints/sec\n", M_DIMINFO.MaxModIntsPerSec, NewMaxModIntsPerSec); } } M_DIMINFO.MaxModIntsPerSec = NewMaxModIntsPerSec; if (pAC->GIni.GIChipId == CHIP_ID_GENESIS) { ModBase = C_CLK_FREQ_GENESIS / pAC->DynIrqModInfo.MaxModIntsPerSec; } else { ModBase = C_CLK_FREQ_YUKON / pAC->DynIrqModInfo.MaxModIntsPerSec; } /* ** We do not need to touch any other registers */ SK_OUT32(pAC->IoBase, B2_IRQM_INI, ModBase); } /******************************************************************************* ** Function : DisplaySelectedModerationType() ** Description : Displays what type of moderation we have ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : void! ** Notes : - *******************************************************************************/ static void DisplaySelectedModerationType(SK_AC *pAC) { if (pAC->DynIrqModInfo.DisplayStats) { if (pAC->DynIrqModInfo.IntModTypeSelect == C_INT_MOD_STATIC) { printk("Static int moderation runs with %i INTS/sec\n", pAC->DynIrqModInfo.MaxModIntsPerSec); } else if (pAC->DynIrqModInfo.IntModTypeSelect == C_INT_MOD_DYNAMIC) { if (IsIntModEnabled(pAC)) { printk("Dynamic int moderation runs with %i INTS/sec\n", pAC->DynIrqModInfo.MaxModIntsPerSec); } else { printk("Dynamic int moderation currently not applied\n"); } } else { printk("No interrupt moderation selected!\n"); } } } /******************************************************************************* ** Function : DisplaySelectedModerationMask() ** Description : Displays what interrupts are moderated ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : void! ** Notes : - *******************************************************************************/ static void DisplaySelectedModerationMask(SK_AC *pAC) { if (pAC->DynIrqModInfo.DisplayStats) { if (pAC->DynIrqModInfo.IntModTypeSelect != C_INT_MOD_NONE) { switch (pAC->DynIrqModInfo.MaskIrqModeration) { case IRQ_MASK_TX_ONLY: printk("Only Tx-interrupts are moderated\n"); break; case IRQ_MASK_RX_ONLY: printk("Only Rx-interrupts are moderated\n"); break; case IRQ_MASK_SP_ONLY: printk("Only special-interrupts are moderated\n"); break; case IRQ_MASK_TX_RX: printk("Tx- and Rx-interrupts are moderated\n"); break; case IRQ_MASK_SP_RX: printk("Special- and Rx-interrupts are moderated\n"); break; case IRQ_MASK_SP_TX: printk("Special- and Tx-interrupts are moderated\n"); break; case IRQ_MASK_RX_TX_SP: printk("All Rx-, Tx and special-interrupts are moderated\n"); break; default: printk("Don't know what is moderated\n"); break; } } else { printk("No specific interrupts masked for moderation\n"); } } } /******************************************************************************* ** Function : DisplayDescrRatio ** Description : Like the name states... ** Programmer : Ralph Roesler ** Last Modified: 23-mar-03 ** Returns : void! ** Notes : - *******************************************************************************/ static void DisplayDescrRatio(SK_AC *pAC) { int TotalMaxNbrDescr = 0; if (pAC->DynIrqModInfo.DisplayStats) { TotalMaxNbrDescr = pAC->RxDescrPerRing * GetRxCalls(pAC); printk("Ratio descriptors: %i/%i\n", M_DIMINFO.NbrProcessedDescr, TotalMaxNbrDescr); } } /******************************************************************************* ** ** End of file ** *******************************************************************************/