2011-03-27 59 views
38

我在使用var_dump()的應用程序中使用了調試幫助,該應用程序使用輸出緩衝來捕獲變量並顯示它們。但是,我遇到了一個大對象的問題,最終會在緩衝區中佔用過多的內存。如何使用var_dump +輸出緩衝沒有內存錯誤?

function getFormattedOutput(mixed $var) { 
    if (isTooLarge($var)) { 
    return 'Too large! Abort!'; // What a solution *might* look like 
    } 

    ob_start(); 
    var_dump($var); // Fatal error: Allowed memory size of 536870912 bytes exhausted 
    $data = ob_get_clean(); 

    // Return the nicely-formated data to use later 
    return $data 
} 

有沒有辦法阻止這種情況?還是解決方法來檢測它是否要爲特定變量輸出大量的信息?我沒有真正控制哪些變量被傳遞給這個函數。它可以是任何類型。

+0

你得到與'print_r',同樣的問題,出於好奇?如果沒有,你是否看到很多遞歸通知? – Charles 2011-03-27 01:42:59

+0

@Charles,可能不是。我*可以*使用'print_r'或'var_export',但我真的很喜歡這個事實,我可以保留'var_dump'提供的變量類型和長度信息。當xdebug可用時,也增加了格式化好處。 – 2011-03-27 01:45:16

+0

由於遞歸,這可能是無限量的輸出。嘗試在相同的變量上自己調用它,而不使用輸出緩衝來查看會發生什麼。 – Jon 2011-03-27 01:45:37

回答

16

好吧,如果物理內存是有限的(你看到的致命錯誤:)

Fatal error: Allowed memory size of 536870912 bytes exhausted

我會建議做磁盤上的輸出緩衝(見ob_start回調參數)。輸出緩衝工作分塊,這意味着,如果仍然有足夠的內存將單個塊保存在內存中,則可以將其存儲到臨時文件中。

// handle output buffering via callback, set chunksize to one kilobyte 
ob_start($output_callback, $chunk_size = 1024); 

但是,您必須記住,這隻會防止緩衝時出現致命錯誤。如果您現在想要返回緩衝區,則仍需要有足夠的內存您可以返回文件句柄或文件路徑,以便還可以對輸出進行流式處理。

但是,您可以使用該文件,然後獲取所需的字節大小。 PHP字符串的開銷並不多,所以如果仍然有足夠的內存用於文件大小,這應該很好。你可以減去偏移量來獲得一個小房間並且玩起來很安全。只是嘗試和錯誤一點點。

一些示例代碼(PHP 5.4):

<?php 
/** 
* @link http://stackoverflow.com/questions/5446647/how-can-i-use-var-dump-output-buffering-without-memory-errors/ 
*/ 

class OutputBuffer 
{ 
    /** 
    * @var int 
    */ 
    private $chunkSize; 

    /** 
    * @var bool 
    */ 
    private $started; 

    /** 
    * @var SplFileObject 
    */ 
    private $store; 

    /** 
    * @var bool Set Verbosity to true to output analysis data to stderr 
    */ 
    private $verbose = true; 

    public function __construct($chunkSize = 1024) { 
     $this->chunkSize = $chunkSize; 
     $this->store  = new SplTempFileObject(); 
    } 

    public function start() { 
     if ($this->started) { 
      throw new BadMethodCallException('Buffering already started, can not start again.'); 
     } 
     $this->started = true; 
     $result = ob_start(array($this, 'bufferCallback'), $this->chunkSize); 
     $this->verbose && file_put_contents('php://stderr', sprintf("Starting Buffering: %d; Level %d\n", $result, ob_get_level())); 
     return $result; 
    } 

    public function flush() { 
     $this->started && ob_flush(); 
    } 

    public function stop() { 
     if ($this->started) { 
      ob_flush(); 
      $result = ob_end_flush(); 
      $this->started = false; 
      $this->verbose && file_put_contents('php://stderr', sprintf("Buffering stopped: %d; Level %d\n", $result, ob_get_level())); 
     } 
    } 

    private function bufferCallback($chunk, $flags) { 

     $chunkSize = strlen($chunk); 

     if ($this->verbose) { 
      $level  = ob_get_level(); 
      $constants = ['PHP_OUTPUT_HANDLER_START', 'PHP_OUTPUT_HANDLER_WRITE', 'PHP_OUTPUT_HANDLER_FLUSH', 'PHP_OUTPUT_HANDLER_CLEAN', 'PHP_OUTPUT_HANDLER_FINAL']; 
      $flagsText = ''; 
      foreach ($constants as $i => $constant) { 
       if ($flags & ($value = constant($constant)) || $value == $flags) { 
        $flagsText .= (strlen($flagsText) ? ' | ' : '') . $constant . "[$value]"; 
       } 
      } 

      file_put_contents('php://stderr', "Buffer Callback: Chunk Size $chunkSize; Flags $flags ($flagsText); Level $level\n"); 
     } 

     if ($flags & PHP_OUTPUT_HANDLER_FINAL) { 
      return TRUE; 
     } 

     if ($flags & PHP_OUTPUT_HANDLER_START) { 
      $this->store->fseek(0, SEEK_END); 
     } 

     $chunkSize && $this->store->fwrite($chunk); 

     if ($flags & PHP_OUTPUT_HANDLER_FLUSH) { 
      // there is nothing to d 
     } 

     if ($flags & PHP_OUTPUT_HANDLER_CLEAN) { 
      $this->store->ftruncate(0); 
     } 

     return ""; 
    } 

    public function getSize() { 
     $this->store->fseek(0, SEEK_END); 
     return $this->store->ftell(); 
    } 

    public function getBufferFile() { 
     return $this->store; 
    } 

    public function getBuffer() { 
     $array = iterator_to_array($this->store); 
     return implode('', $array); 
    } 

    public function __toString() { 
     return $this->getBuffer(); 
    } 

    public function endClean() { 
     return ob_end_clean(); 
    } 
} 


$buffer = new OutputBuffer(); 
echo "Starting Buffering now.\n=======================\n"; 
$buffer->start(); 

foreach (range(1, 10) as $iteration) { 
    $string = "fill{$iteration}"; 
    echo str_repeat($string, 100), "\n"; 
} 
$buffer->stop(); 

echo "Buffering Results:\n==================\n"; 
$size = $buffer->getSize(); 
echo "Buffer Size: $size (string length: ", strlen($buffer), ").\n"; 
echo "Peeking into buffer: ", var_dump(substr($buffer, 0, 10)), ' ...', var_dump(substr($buffer, -10)), "\n"; 

輸出:

STDERR: Starting Buffering: 1; Level 1 
STDERR: Buffer Callback: Chunk Size 1502; Flags 1 (PHP_OUTPUT_HANDLER_START[1]); Level 1 
STDERR: Buffer Callback: Chunk Size 1503; Flags 0 (PHP_OUTPUT_HANDLER_WRITE[0]); Level 1 
STDERR: Buffer Callback: Chunk Size 1503; Flags 0 (PHP_OUTPUT_HANDLER_WRITE[0]); Level 1 
STDERR: Buffer Callback: Chunk Size 602; Flags 4 (PHP_OUTPUT_HANDLER_FLUSH[4]); Level 1 
STDERR: Buffer Callback: Chunk Size 0; Flags 8 (PHP_OUTPUT_HANDLER_FINAL[8]); Level 1 
STDERR: Buffering stopped: 1; Level 0 
Starting Buffering now. 
======================= 
Buffering Results: 
================== 
Buffer Size: 5110 (string length: 5110). 
Peeking into buffer: string(10) "fill1fill1" 
...string(10) "l10fill10\n" 
+0

你可以'fpassthru'生成的文件,這將做分塊讀/寫/刷新,並不佔用大量的內存。 – Leigh 2012-12-19 10:23:00

+2

@李:是的,那是可能的。我現在做了一個概念驗證,沒有使用文件句柄,而是使用了「SplTempFileObject」。從技術上來說'tmp://'同樣可能,可能更好。無論如何,它的工作原理證明。 'SplTempFileObject'和'tmp://'甚至可以將部分流式傳輸到內存中,如果使用了更多的內存,它們將把它放在磁盤上。這可能是最想要的。例如。在內存中保留最多1 MB或類似的內存,磁盤上的較大內存。 – hakre 2012-12-19 14:45:09

+1

我確實接受了這個測試。當Xdebug被啓用時,不要指望它和'var_dump'一起工作。這不起作用,因爲Xdebug會導致整個'var_dump'輸出一次發送,而不是塊,因爲它很常見(請參閱:http://codepad.viper-7.com/PVI5qT - 由@DaveRandom測試)。一些更改的代碼和測試在這裏:https://gist.github.com/4341870 - 最初的想法仍然是一樣的,我只是不想編輯這個問題。 – hakre 2012-12-20 00:11:18

13

當你insall xdebug時,你可以限制var_dump跟在對象後面的深度。在某些軟件產品中,您可能會遇到一種遞歸,這會增加var_dump的輸出。 除此之外,您可以提高內存限制。

http://www.xdebug.org/docs/display

+3

我很欣賞這篇文章,但回答「讓它傾倒更少」只能解決症狀,而不是問題。這個函數可以有一個10維數組,每個數組中只有一個元素 - 應該可以轉儲。但是具有其他巨大物體屬性的類可能會在第二級造成問題。我意識到我在問什麼可能是不可能的 – 2012-12-14 18:49:08

+1

顯然,這只是你擁有多少物理內存的問題。 =) – 2012-12-17 10:02:41

9

我很抱歉,但我覺得這是你的問題沒有解決。您要求確定大小以防止爲該大小分配內存。 PHP不能給你一個關於「它將佔用多少內存」的答案,因爲ZVAL結構是在PHP中使用時創建的。請參閱Programming PHP - 14.5. Memory Management瞭解PHP內存分配內部的概述。

你給出了正確的提示「裏面可以有任何東西」,這是我的觀點。有一個架構問題導致您描述的情況。而且我認爲你試圖在錯誤的一端解決它。

例如:您可以在php中爲每種類型開關,並嘗試設置每種尺寸的限制。只要沒有人想到在這個過程中改變內存限制,這種情況就會持續下去。

Xdebug是一個很好的解決方案,因爲它可以讓應用程序不會因爲(甚至是非業務關鍵型)日誌功能而爆炸,而且它也是一個糟糕的解決方案,因爲您不應該在生產環境中激活xdebug。

我認爲內存異常是正確的行爲,你不應該嘗試解決它。

[言論]如果誰轉儲一個50兆字節以上的字符串不關心他/她的應用程序行爲的人,他/她應該從中受苦;)[/咆哮]

+0

這應該是正確答案。如果這是不可能的,那麼你的方法有什麼問題... – 2012-12-18 15:16:08

5

我不相信有什麼辦法可以確定特定函數最終會佔用多少內存。你可以做的一件事是使用memory_get_usage()來檢查腳本當前正在採用的內存的數量,之後$largeVar被設置,然後將其與之後的數量進行比較。這會給你一個關於$largeVar大小的好主意,你可以運行試驗來確定在你正常退出之前什麼是最大可接受的大小限制。

你也可以自己重新實現var_dump()函數。使函數遍歷結構並在生成內容時回顯生成的內容,或將其存儲在臨時文件中,而不是將巨大的字符串存儲在內存中。這將允許您獲得相同的預期結果,但沒有遇到內存問題。

20

正如所有其他人都提到你所問的是不可能的。你唯一能做的就是儘可能地處理它。

你可以嘗試的是將其拆分成小塊,然後將其組合。我創建了一個小測試來嘗試獲取內存錯誤。顯然,真實世界的例子可能會有不同的表現,但這似乎有訣竅。

<?php 
define('mem_limit', return_bytes(ini_get('memory_limit'))); //allowed memory 

/* 
SIMPLE TEST CLASS 
*/ 
class test { } 
$loop = 260; 
$t = new Test(); 
for ($x=0;$x<=$loop;$x++) { 
    $v = 'test'.$x; 
    $t->$v = new Test(); 
    for ($y=0;$y<=$loop;$y++) { 
    $v2 = 'test'.$y; 
    $t->$v->$v2 = str_repeat('something to test! ', 200); 
    } 
} 
/* ---------------- */ 


echo saferVarDumpObject($t); 

function varDumpToString($v) { 
    ob_start(); 
    var_dump($v); 
    $content = ob_get_contents(); 
    ob_end_clean(); 
    return $content; 
} 

function saferVarDumpObject($var) { 
    if (!is_object($var) && !is_array($var)) 
    return varDumpToString($var); 

    $content = ''; 
    foreach($var as $v) { 
    $content .= saferVarDumpObject($v); 
    } 
    //adding these smaller pieces to a single var works fine. 
    //returning the complete larger piece gives memory error 

    $length = strlen($content); 
    $left = mem_limit-memory_get_usage(true); 

    if ($left>$length) 
    return $content; //enough memory left 

    echo "WARNING! NOT ENOUGH MEMORY<hr>"; 
    if ($left>100) { 
    return substr($content, 0, $left-100); //100 is a margin I choose, return everything you have that fits in the memory 
    } else { 
    return ""; //return nothing. 
    } 
} 

function return_bytes($val) { 
    $val = trim($val); 
    $last = strtolower($val[strlen($val)-1]); 
    switch($last) { 
     // The 'G' modifier is available since PHP 5.1.0 
     case 'g': 
      $val *= 1024; 
     case 'm': 
      $val *= 1024; 
     case 'k': 
      $val *= 1024; 
    } 

    return $val; 
} 
?> 

UPDATE 上面的版本仍然有一些錯誤。我重新創建它使用類和其他一些功能

  • 檢查遞歸
  • 修正了單一的大型屬性
  • 模仿的var_dump輸出
  • 上警示能夠趕上trigger_error /隱藏

如註釋所示,類的資源標識符與var_dump的輸出不同。據我所知,其他事情是平等的。

<?php 
/* 
RECURSION TEST 
*/ 
class sibling { 
    public $brother; 
    public $sister; 
} 
$brother = new sibling(); 
$sister = new sibling(); 
$brother->sister = $sister; 
$sister->sister = $brother; 
Dump::Safer($brother); 


//simple class 
class test { } 

/* 
LARGE TEST CLASS - Many items 
*/ 
$loop = 260; 
$t = new Test(); 
for ($x=0;$x<=$loop;$x++) { 
    $v = 'test'.$x; 
    $t->$v = new Test(); 
    for ($y=0;$y<=$loop;$y++) { 
    $v2 = 'test'.$y; 
    $t->$v->$v2 = str_repeat('something to test! ', 200); 
    } 
} 
//Dump::Safer($t); 
/* ---------------- */ 


/* 
LARGE TEST CLASS - Large attribute 
*/ 
$a = new Test(); 
$a->t2 = new Test(); 
$a->t2->testlargeattribute = str_repeat('1', 268435456 - memory_get_usage(true) - 1000000); 
$a->smallattr1 = 'test small1'; 
$a->smallattr2 = 'test small2'; 
//Dump::Safer($a); 
/* ---------------- */ 

class Dump 
{ 
    private static $recursionhash; 
    private static $memorylimit; 
    private static $spacing; 
    private static $mimicoutput = true; 


    final public static function MimicOutput($v) { 
    //show results similar to var_dump or without array/object information 
    //defaults to similar as var_dump and cancels this on out of memory warning 
    self::$mimicoutput = $v===false ? false : true; 
    } 

    final public static function Safer($var) { 
    //set defaults 
    self::$recursionhash = array(); 
    self::$memorylimit = self::return_bytes(ini_get('memory_limit')); 

    self::$spacing = 0; 

    //echo output 
    echo self::saferVarDumpObject($var); 
    } 

    final private static function saferVarDumpObject($var) { 
    if (!is_object($var) && !is_array($var)) 
     return self::Spacing().self::varDumpToString($var); 

    //recursion check 
    $hash = spl_object_hash($var); 
    if (!empty(self::$recursionhash[$hash])) { 
     return self::Spacing().'*RECURSION*'.self::Eol(); 
    } 
    self::$recursionhash[$hash] = true; 


    //create a similar output as var dump to identify the instance 
    $content = self::Spacing() . self::Header($var); 
    //add some spacing to mimic vardump output 
    //Perhaps not the best idea because the idea is to use as little memory as possible. 
    self::$spacing++; 
    //Loop trough everything to output the result 
    foreach($var as $k=>$v) { 
     $content .= self::Spacing().self::Key($k).self::Eol().self::saferVarDumpObject($v); 
    } 
    self::$spacing--; 
    //decrease spacing and end the object/array 
    $content .= self::Spacing().self::Footer().self::Eol(); 
    //adding these smaller pieces to a single var works fine. 
    //returning the complete larger piece gives memory error 

    //length of string and the remaining memory 
    $length = strlen($content); 
    $left = self::$memorylimit-memory_get_usage(true); 

    //enough memory left? 
    if ($left>$length) 
     return $content; 

    //show warning 
    trigger_error('Not enough memory to dump "'.get_class($var).'" memory left:'.$left, E_USER_WARNING); 
    //stop mimic output to prevent fatal memory error 
    self::MimicOutput(false); 
    if ($left>100) { 
     return substr($content, 0, $left-100); //100 is a margin I chose, return everything you have that fits in the memory 
    } else { 
     return ""; //return nothing. 
    } 
    } 

    final private static function Spacing() { 
    return self::$mimicoutput ? str_repeat(' ', self::$spacing*2) : ''; 
    } 

    final private static function Eol() { 
    return self::$mimicoutput ? PHP_EOL : ''; 
    } 

    final private static function Header($var) { 
    //the resource identifier for an object is WRONG! Its always 1 because you are passing around parts and not the actual object. Havent foundnd a fix yet 
    return self::$mimicoutput ? (is_array($var) ? 'array('.count($var).')' : 'object('.get_class($var).')#'.intval($var).' ('.count((array)$var).')') . ' {'.PHP_EOL : ''; 
    } 

    final private static function Footer() { 
    return self::$mimicoutput ? '}' : ''; 
    } 

    final private static function Key($k) { 
    return self::$mimicoutput ? '['.(gettype($k)=='string' ? '"'.$k.'"' : $k).']=>' : ''; 
    } 

    final private static function varDumpToString($v) { 
    ob_start(); 
    var_dump($v); 

    $length = strlen($v); 
    $left = self::$memorylimit-memory_get_usage(true); 

    //enough memory left with some margin? 
    if ($left-100>$length) { 
     $content = ob_get_contents(); 
     ob_end_clean(); 
     return $content; 
    } 
    ob_end_clean(); 

    //show warning 
    trigger_error('Not enough memory to dump "'.gettype($v).'" memory left:'.$left, E_USER_WARNING); 

    if ($left>100) { 
     $header = gettype($v).'('.strlen($v).')'; 
     return $header . substr($v, $left - strlen($header)); 
    } else { 
     return ""; //return nothing. 
    } 
    } 

    final private static function return_bytes($val) { 
     $val = trim($val); 
     $last = strtolower($val[strlen($val)-1]); 
     switch($last) { 
      // The 'G' modifier is available since PHP 5.1.0 
      case 'g': 
       $val *= 1024; 
      case 'm': 
       $val *= 1024; 
      case 'k': 
       $val *= 1024; 
     } 

     return $val; 
    } 
} 
?> 
+0

有趣。但是,如果對象屬性過於龐大,這將如何表現呢? – Charles 2012-12-19 00:49:00

+0

試過並失敗:)更新了似乎工作的更廣泛的版本。 – 2012-12-19 09:37:53