Steffen Peters: Wie auf Ende einer Javascript-Funktion warten?

Hallo,

über eine Funktion zerteile ich eine große Datei in Einzelteile (Chunks), um dies hochzuladen.

Mit einer kleinen Chunkgröße funktioniert es, aber wenn ich die Chunk-Größe erhöhe, funktioniert es nicht mehr. Ich vermute sehr stark, dass Javascript nicht mehr schnell genug ist, um den Chunk zurückzuliefern und die nachfolgende Funktion kann nicht mit dem Ergebnis arbeiten.

Die bisherige Funktion sieht wie folgt aus:

function slice(file, start, end) {
	var slice = file.mozSlice ? file.mozSlice :
		file.webkitSlice ? file.webkitSlice :
		file.slice ? file.slice : noop;
	return slice.bind(file)(start, end);
}

Und wird wie folgt aufgerufen:

var s = slice(file, start, end);
scount ++;
send(s,start,end,scount,size,num_of_slices,ext,filename,chunkid);
start += sliceSize;

Ich habe bisher noch keine Erfahrungen mit asynchronem Javascript gemacht und tue mich augenscheinlich mit den gefundenen Tutorials schwer, diese auf mein Problem zu übertragen.

Ich habe es wie folgt versucht:

const slice = (file, start, end) => {
	return new Promise((resolve, reject) => {
		var slice = file.mozSlice ? file.mozSlice :
			file.webkitSlice ? file.webkitSlice :
			file.slice ? file.slice : noop;
		resolve (slice.bind(file)(start, end));
	})
}
Und der Aufruf:
	slice(file, start, end).then(result => {
		scount ++;
		send(s,start,end,scount,size,num_of_slices,ext,filename,chunkid);
		start += sliceSize;
	});

Dieser läuft aber nun in einer Endlosschleife, zur Funktion send() kommt das Script offenbar nie.

Was mache ich hier falsch?

LG Steffen

  1. Hallo Steffen,

    wir reden von der Methode Blob.prototype.slice, die an File-Objekte vererbt, wird, ja?

    Diese liefert einen neuen Blob mit den abgetrennten Daten zurück. Das heißt: sie ist synchron, wenn sie zurückkommt, ist sie fertig und Du brauchst nicht zu warten. Ob sie für ihren Job richtig viel Zeit braucht, weiß ich nicht. Vermutlich nicht, ein Megabyte an Daten ist im Speicher fix kopiert.

    D.h. das Timing von slice ist irrelevant. Die Frage ist vielmehr, was send tut. Diese Funktion ist offenbar von Dir, und wenn dort per fetch oder XMLHttpRequest etwas gesendet wird, dann müsstest Du auf das Ende des Sendevorgangs warten bevor Du den nächsten send auslöst. Ich nehme an, dass deine serverseitige Verarbeitung aus dem Tritt kommt, wenn ein zweiter Send eintrifft, bevor der erste durch ist.

    Womit sendest Du? fetch oder XMLHttpRequest?

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      die ganze Schleife zur Abarbeitung aller gewählten Dateien, sieht so aus:

      
      	allfiles.forEach(function (nextfile, index) {
      		file = nextfile;
      		scount = 0;
      		sliceSize = 250000;
      		size = file.size;
      		filename = file.filepath;
      		ext =  "." + file.name.split('.').pop();
      		chunkid = makeid();
      		start = 0;
      		num_of_slices = Math.ceil(size / sliceSize);
      		do {
      			var end = start + sliceSize;
      			if (size - end < 0) {
      				end = size;
      			}
      			var s = slice(file, start, end);
      			scount ++;
      			send(s,start,end,scount,size,num_of_slices,ext,filename,chunkid);
      			start += sliceSize;
      		}
      		while (end < size);
      	});
      
      

      Und ja, im send() sende ich per XMLHttpRequest die Daten. Allerdings funktioniert es reproduzierbar (und nahezu problemlos) bei einem slizeSize von 250000. Nutze ich bspw. 2500000 (eine 0 mehr), dann funktioniert es nicht mehr. D.h. im upload-Script fehlt dann bspw. das tmp_name um die Datei im Filesystem abzulegen.

      Später werde ich mich mit dem XMLHttpRequest wohl auch nochmal beschäftigen müssen, wie ich die Anzahl gleichzeitiger Requests reduziert bekommen. Bei sehr großen Dateien (bspw. > 6GB) reagiert der Server nicht mehr bis der Upload fertiggestellt ist. Der Upload selber läuft aber erfolgreich durch.

      LG Steffen

      1. Hi,

        Und ja, im send() sende ich per XMLHttpRequest die Daten. Allerdings funktioniert es reproduzierbar (und nahezu problemlos) bei einem slizeSize von 250000. Nutze ich bspw. 2500000 (eine 0 mehr), dann funktioniert es nicht mehr.

        Dann wird am Server oder im PHP irgendwo die Request Size begrenzt.

        cu,
        Andreas a/k/a MudGuard

        1. Lieber MudGuard,

          Dann wird am Server oder im PHP irgendwo die Request Size begrenzt.

          insbesondere das PHP-Setting upload_max_filesize wäre zu prüfen. Eventuell scheitert der Upload der Chunks daran.

          Weiteres Problem: Fehlerbehandlung! Auf JS-Seite werden keinerlei HTTP-Fehlercodes behandelt, sodass man als Anwender keinerlei Hinweis erhält, ob der Upload nun geklappt hat, oder nicht - und warum.

          Liebe Grüße

          Felix Riesterer

          1. Hallo Felix,

            Dann wird am Server oder im PHP irgendwo die Request Size begrenzt.

            insbesondere das PHP-Setting upload_max_filesize wäre zu prüfen. Eventuell scheitert der Upload der Chunks daran.

            Mit ca. 2,38MB Chunksize sollte das PHP-Setting (steht auf 8MB) nicht berührt sein.

            Weiteres Problem: Fehlerbehandlung! Auf JS-Seite werden keinerlei HTTP-Fehlercodes behandelt, sodass man als Anwender keinerlei Hinweis erhält, ob der Upload nun geklappt hat, oder nicht - und warum.

            Beim XMLHttpRequest prüfe ich auf bspw. Fehler (xhr.upload.onerror). Ich habe durch Prüfung im PHP-Upload-Script herausgefunden, dass das file-Objekt im Fehlerfall nicht die benötigten Felder enthält (insbesondere tmp_name). Dadurch scheitert zwangsläufig das Speichern.

            move_uploaded_file(
                $tmpname,
                $chunked_file
            );
            

            Wenn ich die Größe der Chunks wieder verkleinere (250000 -> 0,23MB), dann funktioniert der Upload.

            LG Steffen

        2. Hallo Andreas,

          Dann wird am Server oder im PHP irgendwo die Request Size begrenzt.

          die Request Size steht auf 8MB. "2500000" ergibt in etwa 2,38MB, sollte also doch kein Hindernis sein.

          LG Steffen

      2. Hallo Steffen,

        ja, wie gesagt: Du musst auf das Ende des send warten. D.h. du musst Dich beim XMLHttpRequest Objekt (nehmen wir mal an, es steht in der Variablen xhr) auf readystatechange registrieren und bei xhr.readyState === XMLHttpRequest.DONE und xhr.status === 200 ist der Upload des Chunks fertig.

        Das kannst Du manuell in ein Promise verpacken - oder Du verwendest statt XMLHttpRequest gleich das fetch API, das liefert die Promises fertig an.

        Da Du die Slices wohl nur deshalb bildest, um dem Server keine zu großen Brocken auf einmal anzuliefern, solltest Du immer erst dann einen neuen Send starten, wenn der alte durch ist. Eine Parallelisierung der Requests bringt eigentlich nichts, denn der limitierende Faktor für das Upload-Tempo dürfte die Bandbreite des Clients sein und das verbesserst Du nicht, indem Du die Bandbreite auf zwei Transfers aufteilst.

        Bei einer Parallelisierung, d.h. zwei oder mehr Requests gleichzeitig, kommt erschwerend hinzu, dass Dir niemand die Reihenfolge garantiert, in der der Server zwei gleichzeitig vorliegende Requests abarbeitet. Da Du sie nacheinander schickst, ist die Wahrscheinlichkeit extrem hoch, dass sie in dieser Reihenfolge ankommen und auch in dieser Reihenfolge verarbeitet werden. Es ist jedoch nicht garantiert.

        Mit etwas Programmiergeschick kannst Du aber in der Zeit, wo Du auf die Fertigstellung des vorigen POSTs wartest, den nächsten Slice vorbereiten.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo Rolf,

          ja, wie gesagt: Du musst auf das Ende des send warten. D.h. du musst Dich beim XMLHttpRequest Objekt (nehmen wir mal an, es steht in der Variablen xhr) auf readystatechange registrieren und bei xhr.readyState === XMLHttpRequest.DONE und xhr.status === 200 ist der Upload des Chunks fertig.

          empfiehlst du den typsicheren Vergleich hier aus einem konkreten Grund? Hast du in dem Zusammenhabg schon mal einen Brechreiz bei Huftieren erlebt?

          Davon unabhängig: Auch ein Status, der von 200 (OK) abweicht, verdient besondere Beachtung - besonders, aber nicht nur beim ersten Chunk. Rennt man mit dem ersten Chunk in einen Fehler (4xx oder 5xx), kann man eigentlich gleich den ganzen Upload-Vorgang abpfeifen. Tritt zwischendurch ein Fehler auf, muss man im Einzelfall entscheiden, ob es sich lohnt, den Chunk zu wiederholen, oder ob man besser abbrechen sollte.

          Da Du die Slices wohl nur deshalb bildest, um dem Server keine zu großen Brocken auf einmal anzuliefern, solltest Du immer erst dann einen neuen Send starten, wenn der alte durch ist. Eine Parallelisierung der Requests bringt eigentlich nichts, denn der limitierende Faktor für das Upload-Tempo dürfte die Bandbreite des Clients sein und das verbesserst Du nicht, indem Du die Bandbreite auf zwei Transfers aufteilst.

          Guter Punkt.

          Bei einer Parallelisierung, d.h. zwei oder mehr Requests gleichzeitig, kommt erschwerend hinzu, dass Dir niemand die Reihenfolge garantiert, in der der Server zwei gleichzeitig vorliegende Requests abarbeitet. Da Du sie nacheinander schickst, ist die Wahrscheinlichkeit extrem hoch, dass sie in dieser Reihenfolge ankommen und auch in dieser Reihenfolge verarbeitet werden. Es ist jedoch nicht garantiert.

          Genua. TCP/IP hat kein deterministisches Zeitverhalten. Das bedeutet aber auch, dass das PHP-Script, das sie Chunks entgegennimmt, für jeden einzelnen Request wissen muss, welchem Datei-Offset das entspricht. Und es muss (entweder beim ersten, besser noch bei jedem Chunk) eine Angabe der Gesamt-Dateigröße bekommen, damit es zu gegebener Zeit feststellen kann: Jetzt hab ich alles und kann die fertige Datei wegspeichern.

          Mit etwas Programmiergeschick kannst Du aber in der Zeit, wo Du auf die Fertigstellung des vorigen POSTs wartest, den nächsten Slice vorbereiten.

          Das wäre cool, ja.

          Einen schönen Tag noch
           Martin

          --
          Es liegt allein an uns, ob wir aus den vielen Steinen, die wir einander in den Weg legen, Mauern oder Brücken bauen. (Ernst Ferstl)
          1. Hallo Der,

            xhr.readyState === XMLHttpRequest.DONE und xhr.status === 200 ist der Upload des Chunks fertig.

            empfiehlst du den typsicheren Vergleich hier aus einem konkreten Grund?

            Nö. Ich hatte das irgendwoher kopiert und da war der === drin. Einfach nicht drüber nachgedacht. Falsch ist's auf jeden Fall nicht.

            Rolf

            --
            sumpsi - posui - obstruxi
        2. Hallo Rolf,

          eigentlich fand ich das ganz nett, dass bei der Fortschrittsanzeige des Uploads nicht immer nur eine Datei nach der anderen hochgeladen wird.

          Für den Upload erstelle ich für jede Datei eine eindeutige ID ($file_id), merke mir die Anzahl der benötigten Chunks ($num_chunks_created) und jeder Chunk erhält eine fortlaufende Nummer.

          im PHP-Upload-Script speichere ich den aktuellen Chunk, zähle dann, ob alle Chunks vorhanden sind ($chunksUploaded) und merge sie dann wieder zusammen.

          if ($chunksUploaded == $num_chunks_created) {
              for ($i = 1; $i <= $num_chunks_created; $i++) {
              		$chunkname = $target_path.$chunk_location.$file_id.'-'.$i.$extension;
          		
              		$file = fopen($chunkname, 'rb');
              		$buff = fread($file, filesize($chunkname));
              		fclose($file);
          
              		$final = fopen($target_file, 'ab');
              		$write = fwrite($final, $buff);
              		fclose($final);
          
              		unlink($target_path.$chunk_location.$file_id.'-'.$i.$extension);
              }
          }
          

          Die Funktion send() sieht im Moment so aus:

          function send(piece,start,end,scount,size,num_of_slices,ext,filename,chunkid) {
          	var formdata = new FormData();
          	var xhr = new XMLHttpRequest();
          
          	xhr.open('POST', 'upload.php', true);
          
          	formdata.append('MAX_FILE_SIZE', 1000000);
          	formdata.append('start', start);
          	formdata.append('end', end);
          	formdata.append('file', piece, filename);
          	formdata.append('scount', scount);
          	formdata.append('fsize', size);
          	formdata.append('num_of_slices', num_of_slices);
          	formdata.append('ext', ext);
          	formdata.append('filename', filename);
          	formdata.append('chunkid', chunkid);
          	formdata.append('folder', folder);
          	xhr.upload.onerror = function() {
          		alert("Error during upload! "+xhr.status);
          	};
          	xhr.upload.onprogress = function (e) {
          //		updateProgressFile(filename, e.loaded, proz);
          	};
          	xhr.onload = function() {
          		console.log(xhr.responseText);		// debug info
          		updateProgressTotal(filename);
          		checkcompleted();
          	};
          	xhr.send(formdata);
          }
          

          Auf fetch umzustellen oder sicherzustellen, dass immer nur 1 Upload läuft, wird wohl mein aktuelles Problem nicht lösen, da ja der Upload bei einer kleinen Chunk-Größe (0,23MB) funktioniert und bei einer Größe von 2,38MB nicht mehr. Meine Vermutung war, dass das Slice nicht schnell genug ist, aber wenn nicht, dann weiß ich nicht, warum die Größe des Chunks einen Unterschied macht.

          LG Steffen

          1. Fehler gefunden!

            formdata.append('MAX_FILE_SIZE', 1000000);
            

            war die Ursache! Manchmal sieht man den Wald vor lauter Bäumen nicht. Habe nun auch auf fetch umgestellt und werde nun mit großen Dateien und Verzeichnissen testen.

            LG Steffen