การค้นหาหน่วยความจำ JavaScript รั่วไหลด้วย Chrome


163

ฉันได้สร้างกรณีทดสอบง่ายๆที่สร้างมุมมอง Backbone แนบตัวจัดการกับเหตุการณ์และสร้างคลาสที่ผู้ใช้กำหนดเอง ฉันเชื่อว่าด้วยการคลิกปุ่ม "ลบ" ในตัวอย่างนี้ทุกอย่างจะถูกล้างและไม่ควรมีหน่วยความจำรั่ว

jsfiddle สำหรับโค้ดอยู่ที่นี่: http://jsfiddle.net/4QhR2/

// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

อย่างไรก็ตามฉันไม่แน่ใจว่าจะใช้ผู้สร้างโปรไฟล์ของ Google Chrome ได้อย่างไรเพื่อตรวจสอบว่าเป็นจริงในกรณีนี้ มีสิ่งหนึ่งล้านล้านที่ปรากฏบนสแนปชอตของผู้สร้างฮีปและฉันไม่รู้ว่าจะถอดรหัสดีหรือไม่ดีได้อย่างไร บทเรียนที่ฉันได้เห็นจนถึงตอนนี้ก็แค่บอกให้ฉัน "ใช้สแน็ปช็อตโปรไฟล์" หรือให้รายละเอียดอย่างละเอียดเกี่ยวกับการทำงานของโปรไฟล์ทั้งหมด เป็นไปได้หรือไม่ที่จะใช้ profiler เป็นเครื่องมือหรือฉันต้องเข้าใจจริงๆว่าทุกอย่างถูกออกแบบมาอย่างไร?

แก้ไข:บทแนะนำเช่นนี้:

การแก้ไขการรั่วไหลของหน่วยความจำ Gmail

ใช้ DevTools

เป็นตัวแทนของวัสดุที่แข็งแรงกว่าจากที่ฉันเคยเห็น อย่างไรก็ตามนอกเหนือจากการแนะนำแนวคิดของ3 Snapshot Techniqueฉันพบว่าพวกเขามีความรู้เชิงปฏิบัติน้อยมาก (สำหรับผู้เริ่มต้นอย่างฉัน) บทแนะนำ 'การใช้ DevTools' ไม่ทำงานผ่านตัวอย่างจริงดังนั้นคำอธิบายที่คลุมเครือและแนวคิดทั่วไปเกี่ยวกับสิ่งต่าง ๆ จึงไม่เป็นประโยชน์มากนัก สำหรับตัวอย่าง 'Gmail':

ดังนั้นคุณพบการรั่วไหล ตอนนี้คืออะไร

  • ตรวจสอบเส้นทางการยึดของวัตถุที่รั่วไหลในครึ่งล่างของพาเนลโปรไฟล์

  • หากไซต์การจัดสรรไม่สามารถอนุมานได้ง่าย (เช่นผู้ฟังเหตุการณ์):

  • จัดทำเครื่องมือสร้างของวัตถุการเก็บรักษาผ่านคอนโซล JS เพื่อบันทึกการติดตามสแต็กสำหรับการจัดสรร

  • ใช้ปิดหรือไม่ เปิดใช้งานการตั้งค่าสถานะที่มีอยู่ที่เหมาะสม (เช่น goog.events.Listener.ENABLE_MONITORING) เพื่อตั้งค่าคุณสมบัติ creationStack ในระหว่างการก่อสร้าง

ฉันพบว่าตัวเองสับสนมากขึ้นหลังจากอ่านแล้วไม่น้อย และอีกครั้งมันเป็นเพียงแค่บอกให้ฉันทำสิ่งที่ไม่ว่าจะทำพวกเขา จากมุมมองของฉันข้อมูลทั้งหมดที่ออกมามีความคลุมเครือเกินไปหรือไม่เหมาะสมกับคนที่เข้าใจกระบวนการนี้อยู่แล้ว

ประเด็นที่เฉพาะเจาะจงเหล่านี้บางอย่างได้รับการหยิบยกขึ้นมาในคำตอบของ @Jonathan Naguinด้านล่าง


2
ฉันไม่รู้อะไรเลยเกี่ยวกับการทดสอบการใช้หน่วยความจำในเบราว์เซอร์ แต่ในกรณีที่คุณไม่ได้เห็นบทความของ Addy Osmani เกี่ยวกับตัวตรวจสอบเว็บ Chromeอาจมีประโยชน์
Paul D. Waite

1
ขอบคุณสำหรับคำแนะนำพอล อย่างไรก็ตามเมื่อฉันถ่ายรูปหนึ่งภาพก่อนคลิกลบจากนั้นคลิกอีกครั้งหลังจากคลิกแล้วเลือก 'วัตถุที่จัดสรรระหว่างภาพรวม 1 และ 2' (ตามที่แนะนำในบทความของเขา) ยังมีวัตถุมากกว่า 2,000 รายการ ตัวอย่างเช่นมี 4 รายการ 'HTMLButtonElement' ซึ่งไม่มีเหตุผลสำหรับฉัน อย่างแท้จริงฉันไม่รู้ว่าเกิดอะไรขึ้น
EleventyOne

3
ไม่เป็นประโยชน์เลย อาจเป็นเพราะภาษาที่รวบรวมขยะเช่น JavaScript คุณไม่ได้ตั้งใจจะยืนยันสิ่งที่คุณกำลังทำกับหน่วยความจำในระดับที่ละเอียดเหมือนการทดสอบของคุณ วิธีที่ดีกว่าในการตรวจสอบการรั่วไหลของหน่วยความจำอาจเป็นการโทรmain10,000 ครั้งแทนที่จะเป็นหนึ่งครั้งและดูว่าคุณใช้หน่วยความจำที่เหลืออีกมากในตอนท้ายหรือไม่
Paul D. Waite

3
@ PaulD.Waite ใช่บางที แต่สำหรับฉันแล้วฉันยังต้องการการวิเคราะห์ระดับละเอียดเพื่อตรวจสอบว่าปัญหาคืออะไรแทนที่จะพูดว่า (หรือไม่พูด): "โอเคมีปัญหาเรื่องความจำอยู่ที่นี่" และฉันได้รับความประทับใจที่ฉันควรจะสามารถใช้ผู้สร้างโปรไฟล์ของพวกเขาในระดับที่ละเอียดมาก ... ฉันแค่ไม่แน่ใจว่า :(
EleventyOne

คุณควรดูที่youtube.com/watch?v=L3ugr9BJqIs
maja

คำตอบ:


205

เวิร์กโฟลว์ที่ดีในการค้นหารอยรั่วของหน่วยความจำคือเทคนิคสแนปชอตสามวิธีที่ใช้ครั้งแรกโดย Loreena Lee และทีม Gmail เพื่อแก้ปัญหาหน่วยความจำบางส่วน โดยทั่วไปขั้นตอนคือ:

  • ถ่ายภาพกองขนาดใหญ่
  • ทำสิ่งต่างๆ
  • ใช้ภาพรวมกองอื่น
  • ทำซ้ำสิ่งเดียวกัน
  • ใช้ภาพรวมกองอื่น
  • กรองวัตถุที่จัดสรรระหว่าง Snapshots 1 และ 2 ในมุมมอง "สรุป" ของ Snapshot 3

สำหรับตัวอย่างของคุณฉันได้ดัดแปลงโค้ดเพื่อแสดงขั้นตอนนี้ (คุณสามารถหาได้ที่นี่ ) ชะลอการสร้าง Backbone View จนกว่าเหตุการณ์คลิกของปุ่ม Start ขณะนี้:

  • เรียกใช้ HTML (บันทึกไว้ในเครื่องโดยใช้ที่อยู่นี้) และทำการจับภาพ
  • คลิกเริ่มเพื่อสร้างมุมมอง
  • ถ่ายภาพสแนปชอตอื่น
  • คลิกลบ
  • ถ่ายภาพสแนปชอตอื่น
  • กรองวัตถุที่จัดสรรระหว่าง Snapshots 1 และ 2 ในมุมมอง "สรุป" ของ Snapshot 3

ตอนนี้คุณพร้อมที่จะค้นหาหน่วยความจำรั่ว!

คุณจะสังเกตเห็นโหนดที่มีสีต่างกันเล็กน้อย โหนดสีแดงไม่มีการอ้างอิงโดยตรงจาก Javascript ไปยังพวกเขา แต่ยังมีชีวิตอยู่เพราะพวกเขาเป็นส่วนหนึ่งของต้นไม้ DOM แยก อาจมีโหนดในแผนผังที่อ้างอิงจาก Javascript (อาจเป็นปิดหรือตัวแปร) แต่มีการป้องกันไม่ให้ต้นไม้ DOM ทั้งต้นถูกรวบรวมโดยไม่ได้ตั้งใจ

ป้อนคำอธิบายรูปภาพที่นี่

โหนดสีเหลืองมีการอ้างอิงโดยตรงจาก Javascript ค้นหาโหนดสีเหลืองในแผนผัง DOM เดี่ยวที่แยกออกเพื่อค้นหาการอ้างอิงจาก Javascript ของคุณ ควรมีห่วงโซ่ของคุณสมบัติที่นำจากหน้าต่าง DOM ไปยังองค์ประกอบ

โดยเฉพาะของคุณคุณสามารถเห็นองค์ประกอบ HTML Div ทำเครื่องหมายเป็นสีแดง หากคุณขยายองค์ประกอบคุณจะเห็นว่ามีการอ้างอิงโดยฟังก์ชัน "แคช"

ป้อนคำอธิบายรูปภาพที่นี่

เลือกแถวและในประเภทคอนโซลของคุณ $ 0 คุณจะเห็นฟังก์ชั่นและสถานที่จริง:

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

นี่คือที่ที่องค์ประกอบของคุณถูกอ้างอิง โชคไม่ดีที่คุณทำไม่ได้มากนักมันเป็นกลไกภายในจาก jQuery แต่เพื่อการทดสอบเท่านั้นให้ไปที่ฟังก์ชั่นและเปลี่ยนวิธีการเป็น:

function cache( key, value ) {
    return value;
}

ตอนนี้ถ้าคุณทำซ้ำกระบวนการคุณจะไม่เห็นโหนดสีแดงใด ๆ :)

เอกสารอ้างอิง:


8
ฉันขอขอบคุณความพยายามของคุณ อันที่จริงเทคนิคสแนปช็อตทั้งสามถูกกล่าวถึงเป็นประจำในแบบฝึกหัด น่าเสียดายที่รายละเอียดมักถูกลบออกไป ยกตัวอย่างเช่นฉันขอขอบคุณที่แนะนำของ$0ฟังก์ชั่นในคอนโซลที่เป็นของใหม่กับฉัน - แน่นอนผมมีความคิดว่าว่าทำหรือไม่มีวิธีการที่คุณรู้ว่าจะใช้มัน ( $1ดูเหมือนว่าไร้ประโยชน์ในขณะที่$2ดูเหมือนว่าจะทำในสิ่งเดียวกัน) ประการที่สองคุณรู้จักไฮไลต์แถว#button in function cache()ได้อย่างไรและไม่ใช่แถวอื่น ๆ อีกหลายสิบแถว สุดท้ายมีโหนดสีแดงในNodeListและHTMLInputElementเกินไป แต่ฉันไม่สามารถคิดออก
EleventyOne

7
คุณรู้ได้อย่างไรว่าcacheแถวนั้นมีข้อมูลในขณะที่คนอื่นไม่ได้? มีหลายสาขาที่มีระยะทางที่ต่ำกว่าที่cacheหนึ่ง และผมไม่แน่ใจว่าวิธีการที่คุณรู้ว่าเป็นลูกของHTMLInputElement HTMLDivElementฉันเห็นมันอ้างอิงอยู่ข้างใน ("native ใน HTMLDivElement") แต่มันก็อ้างอิงตัวเองและสองHTMLButtonElements ซึ่งไม่สมเหตุสมผลสำหรับฉัน ฉันขอขอบคุณที่คุณระบุคำตอบสำหรับตัวอย่างนี้ แต่ฉันจะไม่ทราบวิธีการสรุปในประเด็นอื่น ๆ
EleventyOne

2
แปลกมากฉันใช้ตัวอย่างของคุณและฉันได้ผลลัพธ์ที่แตกต่างจากที่คุณทำ (จากภาพหน้าจอของคุณ) อย่างไรก็ตามฉันขอขอบคุณทุกความช่วยเหลือของคุณ ตอนนี้ฉันคิดว่าฉันมีเพียงพอแล้วและเมื่อฉันมีตัวอย่างในชีวิตจริงที่ฉันต้องการความช่วยเหลือเฉพาะฉันจะสร้างคำถามใหม่ที่นี่ ขอบคุณอีกครั้ง.
EleventyOne

2
สามารถดูคำอธิบายของ $ 0 ได้ที่นี่: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta

4
อะไรFilter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.หมายถึง?
K - ความเป็นพิษใน SO กำลังเพิ่มขึ้น

8

ต่อไปนี้เป็นเคล็ดลับเกี่ยวกับการสร้างโปรไฟล์หน่วยความจำของ jsfiddle: ใช้ URL ต่อไปนี้เพื่อแยกผลลัพธ์ jsfiddle ของคุณมันจะลบเฟรมเวิร์ก jsfiddle ทั้งหมดและโหลดเฉพาะผลลัพธ์ของคุณ

http://jsfiddle.net/4QhR2/show/

ฉันไม่สามารถหาวิธีใช้ Timeline และ Profiler เพื่อติดตามการรั่วไหลของหน่วยความจำจนกว่าฉันจะอ่านเอกสารต่อไปนี้ หลังจากอ่านส่วนที่ชื่อว่า 'ตัวติดตามการจัดสรรวัตถุ' ฉันสามารถใช้เครื่องมือ 'บันทึกการจัดสรรฮีป' และติดตามโหนด DOM ที่แยกออกบางส่วน

ฉันแก้ไขปัญหาด้วยการสลับจากการโยงเหตุการณ์ jQuery เป็นการใช้การมอบหมายเหตุการณ์ Backbone View.remove()มันเป็นความเข้าใจของฉันว่ารุ่นใหม่ของกระดูกสันหลังจะยกเลิกการเชื่อมโยงโดยอัตโนมัติเหตุการณ์ที่เกิดขึ้นสำหรับคุณถ้าคุณเรียก ดำเนินการสาธิตบางอย่างด้วยตนเองพวกเขาจะตั้งค่าด้วยการรั่วไหลของหน่วยความจำเพื่อให้คุณระบุ อย่าลังเลที่จะถามคำถามที่นี่หากคุณยังไม่ได้รับหลังจากศึกษาเอกสารนี้

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling


6

โดยทั่วไปคุณต้องดูจำนวนวัตถุในสแนปชอตฮีปของคุณ หากจำนวนวัตถุเพิ่มขึ้นระหว่างสองสแนปชอตและคุณได้ทิ้งออบเจ็กต์แสดงว่าคุณมีหน่วยความจำรั่ว คำแนะนำของฉันคือการมองหาตัวจัดการเหตุการณ์ในรหัสของคุณซึ่งไม่ได้แยกออก


3
ตัวอย่างเช่นถ้าฉันดู heap snapshot ของ jsfiddle ก่อนที่ฉันจะคลิก 'Remove' จะมีวัตถุมากกว่า 100,000 รายการ ฉันจะหาวัตถุที่สร้างรหัส jsfiddle ของฉันได้จากที่ไหน? ฉันคิดว่ามันWindow/http://jsfiddle.net/4QhR2/showอาจจะมีประโยชน์ แต่มันก็เป็นฟังก์ชั่นที่ไม่รู้จบ ฉันไม่รู้ว่าเกิดอะไรขึ้นในนั้น
EleventyOne

@Eventventura: ฉันจะไม่ใช้ jsFiddle ทำไมไม่สร้างไฟล์บนคอมพิวเตอร์ของคุณเองเพื่อทำการทดสอบ
Blue Skies

1
@ BlueSkies ฉันสร้าง jsfiddle เพื่อให้ผู้คนที่นี่สามารถทำงานได้จาก codebase เดียวกัน อย่างไรก็ตามเมื่อฉันสร้างไฟล์บนคอมพิวเตอร์ของฉันเองเพื่อทำการทดสอบยังมีวัตถุมากกว่า 50,000 รายการในสแนปช็อตฮีป
EleventyOne

@EleventyOne สแนปชอตหนึ่งฮีปไม่ได้ให้แนวคิดว่าคุณมีหน่วยความจำรั่วหรือไม่ คุณต้องการอย่างน้อยสอง
Konstantin Dinev

2
จริง ฉันเน้นว่ามันยากเพียงใดที่จะรู้ว่าจะต้องค้นหาเมื่อมีวัตถุนับพันปรากฏขึ้น
EleventyOne


3

คุณสามารถดูที่แท็บ Timeline ในเครื่องมือสำหรับนักพัฒนา บันทึกการใช้งานแอปของคุณและคอยดูจำนวน DOM Node และ Event Listener

หากกราฟหน่วยความจำจะบ่งบอกถึงการรั่วไหลของหน่วยความจำอย่างแน่นอนจากนั้นคุณสามารถใช้ profiler เพื่อหาสิ่งที่รั่วไหล


3

คุณอาจต้องการอ่าน:

http://addyosmani.com/blog/taming-the-unicorn-easing-javascript-memory-profiling-in-devtools/

มันอธิบายการใช้เครื่องมือสำหรับนักพัฒนาของ Chrome และให้คำแนะนำทีละขั้นตอนเกี่ยวกับวิธีการยืนยันและค้นหาการรั่วไหลของหน่วยความจำโดยใช้การเปรียบเทียบสแนปชอตของฮีปและมุมมองภาพสแนปชอตตับที่แตกต่างกัน


2

ฉันขอแนะนำให้ใช้สแน็ปช็อตกองที่สองพวกเขายอดเยี่ยมในการตรวจหารอยรั่วของหน่วยความจำโครเมี่ยมทำงานได้อย่างยอดเยี่ยมในการถ่ายภาพสแนปช็อต

ในโครงการวิจัยสำหรับปริญญาของฉันฉันกำลังสร้างเว็บแอปพลิเคชันแบบอินเทอร์แอคทีฟที่ต้องสร้างข้อมูลจำนวนมากที่สร้างขึ้นใน 'เลเยอร์' เลเยอร์เหล่านี้จำนวนมากจะถูก 'ลบ' ใน UI แต่ด้วยเหตุผลบางอย่าง ถูกยกเลิกการจัดสรรโดยใช้เครื่องมือสแนปชอตฉันสามารถตรวจสอบได้ว่า JQuery ทำการอ้างอิงบนวัตถุ (แหล่งที่มาคือตอนที่ฉันพยายามเรียกใช้.load()เหตุการณ์ที่เก็บการอ้างอิงแม้จะอยู่นอกขอบเขต) การมีข้อมูลนี้อยู่ในมือช่วยบันทึกโครงการของฉันเพียงลำพังมันเป็นเครื่องมือที่มีประโยชน์อย่างมากเมื่อคุณใช้ห้องสมุดของคนอื่นและคุณมีปัญหาในการอ้างอิงที่ยั่วยุให้ GC ไม่สามารถทำงานได้

แก้ไข: นอกจากนี้ยังมีประโยชน์ในการวางแผนล่วงหน้าว่าการกระทำใดที่คุณจะทำเพื่อลดเวลาที่ใช้ในการถ่ายภาพรวมตั้งสมมติฐานว่าอะไรที่อาจทำให้เกิดปัญหาและทดสอบแต่ละสถานการณ์ออกมาทำภาพรวมก่อนและหลัง


0

หมายเหตุสำคัญสองประการเกี่ยวกับการระบุการรั่วไหลของหน่วยความจำโดยใช้เครื่องมือของนักพัฒนาซอฟต์แวร์ Chrome:

1) Chrome เองมีหน่วยความจำรั่วสำหรับองค์ประกอบบางอย่างเช่นรหัสผ่านและฟิลด์หมายเลข https://bugs.chromium.org/p/chromium/issues/detail?id=967438 หลีกเลี่ยงการใช้สิ่งเหล่านั้นขณะทำการดีบั๊กขณะที่โพลสแนปช็อตฮีปของคุณเมื่อค้นหาองค์ประกอบที่แยก

2) หลีกเลี่ยงการบันทึกสิ่งที่คอนโซลเบราว์เซอร์ Chrome จะไม่เก็บรวบรวมวัตถุที่เขียนลงในคอนโซลจึงมีผลกับผลลัพธ์ของคุณ คุณสามารถระงับเอาต์พุตโดยวางโค้ดต่อไปนี้ไว้ในตอนต้นของสคริปต์ / หน้า:

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3) ใช้ heap snapshots และค้นหา "detach" เพื่อระบุองค์ประกอบ DOM ที่แยกออกมา ด้วยการโฮเวอร์วัตถุคุณจะสามารถเข้าถึงคุณสมบัติทั้งหมดรวมถึงidและouterHTMLซึ่งอาจช่วยระบุแต่ละองค์ประกอบ สกรีนช็อตของ JS Heap Snapshot พร้อมรายละเอียดเกี่ยวกับองค์ประกอบ DOM ที่แยกออกมา หากองค์ประกอบที่แยกออกมานั้นยังคงกว้างเกินกว่าจะรับรู้ได้ให้กำหนด ID เฉพาะให้พวกเขาโดยใช้คอนโซลของเบราว์เซอร์ก่อนทำการทดสอบของคุณเช่น:

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

ตอนนี้เมื่อคุณระบุองค์ประกอบเดี่ยว ๆ ด้วยให้พูด id = "AutoId_49" โหลดหน้าของคุณดำเนินการตัวอย่างด้านบนอีกครั้งและค้นหาองค์ประกอบที่มี id = "AutoId_49" โดยใช้ตัวตรวจสอบ DOM หรือ document.querySelector (.. ) . โดยปกติจะใช้งานได้ก็ต่อเมื่อเนื้อหาเพจของคุณสามารถคาดเดาได้

ฉันรันการทดสอบเพื่อระบุการรั่วไหลของหน่วยความจำได้อย่างไร

1) โหลดหน้า (ที่มีเอาต์พุตคอนโซลถูกระงับ!)

2) ทำสิ่งต่างๆบนหน้าเว็บซึ่งอาจส่งผลให้เกิดการรั่วไหลของหน่วยความจำ

3) ใช้เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์เพื่อถ่ายภาพกองและค้นหา "แยก"

4) วางองค์ประกอบเพื่อระบุองค์ประกอบจากคุณสมบัติidหรือouterHTML


นอกจากนี้ยังเป็นความคิดที่ดีที่จะปิดใช้งาน minifying / uglifying เนื่องจากทำให้การดีบักในเบราว์เซอร์ยากขึ้น
จิมมี่ทอมเซ่น
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.