Säilytys olla voi valoisampaa
Tämä teksti on osa blogisarjaamme, jossa yksittäiset henkilöt kirjoittavat sekalaisista pitkäaikaissäilytysteemoista vapaamuotoisesti. Blogisarjaamme ei ole tarkoitettu ohjaaviksi säännöiksi tai ohjeiksi, vaan inspiraatioksi ideoille ja keskustelulle.
Always look on the bright side of file. Törmäsimme vastikään tuotannossa siirtopakettiin, jonka validointi aikakatkaistiin. Paketti ei ollut erityisen suuri ja sisälsi DPX-kuvia, joita olemme vastaanottaneet vuosia ilman ongelmia. Paketin tutkimisen yhteydessä meille huomautettiin, että tummasävyisten kuvien käsittely on viisisataa kertaa kirkkaita kuvia nopeampaa.
Tässä blogikirjoituksessa syväluotaamme tapauksen teknisiin yksityiskohtiin. Aloitamme profiloimalla koodin suoritusta, selvitämme hitauden juurisyyn ja viimeiseksi korjaamme sen.
Kuvien kirkkauden varmastikin täytyy olla harhauttava savusilli?
Virheen toistaminen
Saimme tutkittavaksemme yhden tumman ja yhden kirkkaan DPX-kuvan. Kirkkaan kuvan käsittely oli todella 500 kertaa tummaa kuvaa hitaampaa. Alkuperäisten kuvien sijaan olemme luoneet kaksi uutta kuvaa toistamaan saman virheen: tumma.dpx ja kirkas.dpx. Molemmat kuvat on luotu GIMP-työkalulla ja muunnettu ffmpeg-työkalulla DPX-formaattiin. Tumma.dpx on 3840x2160 kanvaasi täytettynä värillä 0x000414 ja kirkas.dpx on tämän negatiivi eli sama kanvaasi täynnä väriä 0xfffbeb.
Voimme toistaa virheen purkamalla kuvat:
# Pura $ gunzip *.gz # Tarkista formaatti $ file *.dpx kirkas.dpx: DPX image data, little-endian, 3840x2160, left to right/top to bottom tumma.dpx: DPX image data, little-endian, 3840x2160, left to right/top to bottom # Laske tarkistussummat $ sha256sum *.dpx 3169f1e6807efe3123c01dd16be7d2c0c8549c1567c879e02bcc02f1f273be36 kirkas.dpx 98b209794a6abf8c45065e462e28978f639fe2ca584a896f496c40e0556067ca tumma.dpx
ja ajamalla ne file-scraper -työkalun läpi:
$ for file in kirkas.dpx tumma.dpx time scraper scrape-file $file > /dev/null end ________________________________________________________ Executed in 125.82 secs fish external usr time 124.99 secs 0.00 micros 124.99 secs sys time 0.19 secs 875.00 micros 0.19 secs ________________________________________________________ Executed in 558.48 millis fish external usr time 381.17 millis 0.00 micros 381.17 millis sys time 185.60 millis 508.00 micros 185.09 millis
Kirkkaan kuvan validoinnissa kuluu kaksi minuuttia ja tumman kuvan validoinnissa puoli sekuntia. Ainoa ero kuvissa on kanvaasin väri.
Python koodin profilointi
File-scraper -työkalu suorittaa taustalla lukuisia eri työkaluja. Hitaan työkalun voi löytää manuaalisesti tutkimalla koodia ja suorittamalla komentoja. Python standardikirjastosta löytyy myös työkaluja, jotka automatisoivat tämän tehtävän. Ohjelmiston suorituskyvyn tutkiminen tunnetaan profilointina ja se onnistuu esimerkiksi cProfile-kirjastolla.
Lähdetään liikkeelle ajamalla kirkas kuva cProfile-kirjaston läpi ja järjestämällä tuloste kumulatiivisen suoritusajan mukaan:
$ python3 -m cProfile -s cumtime /usr/bin/scraper scrape-file kirkas.dpx | head -50 . . <scraper output> . 881009 function calls (860708 primitive calls) in 140.657 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 305/1 0.000 0.000 140.657 140.657 {built-in method builtins.exec} 1 0.000 0.000 140.657 140.657 scraper:3(<module>) 1 0.000 0.000 140.340 140.340 core.py:1126(__call__) 1 0.000 0.000 140.340 140.340 core.py:981(main) 1 0.000 0.000 140.340 140.340 core.py:1625(invoke) 2 0.000 0.000 140.339 70.170 core.py:1384(invoke) 2 0.000 0.000 140.339 70.170 core.py:703(invoke) 1 0.000 0.000 140.339 140.339 cmdline.py:45(scrape_file) 1 0.000 0.000 140.338 140.338 scraper.py:285(scrape) 1 0.000 0.000 140.335 140.335 scraper.py:140(detect_filetype) 7 0.000 0.000 140.334 20.048 scraper.py:464(_update_filetype) 1 0.000 0.000 140.312 140.312 detectors.py:182(detect) 1 0.000 0.000 140.263 140.263 detectors.py:100(identify) 1 0.000 0.000 140.263 140.263 fido.py:340(identify_file) 1 0.003 0.003 140.262 140.262 fido.py:665(match_formats) 40 0.000 0.000 140.107 3.503 re.py:198(search) 56 140.097 2.502 140.097 2.502 {method 'search' of 're.Pattern' objects} 360/3 0.001 0.000 0.323 0.108 <frozen importlib._bootstrap>:1002(_find_and_load) 354/2 0.001 0.000 0.323 0.161 <frozen importlib._bootstrap>:967(_find_and_load_unlocked) 464/3 0.000 0.000 0.322 0.107 <frozen importlib._bootstrap>:220(_call_with_frames_removed) 340/3 0.001 0.000 0.322 0.107 <frozen importlib._bootstrap>:659(_load_unlocked) 303/3 0.001 0.000 0.322 0.107 <frozen importlib._bootstrap_external>:844(exec_module) 1 0.000 0.000 0.316 0.316 cmdline.py:1(<module>) 1 0.000 0.000 0.270 0.270 scraper.py:1(<module>) 94/61 0.000 0.000 0.251 0.004 {built-in method builtins.__import__} 1 0.000 0.000 0.245 0.245 iterator.py:1(<module>) 112/46 0.000 0.000 0.238 0.005 <frozen importlib._bootstrap>:1033(_handle_fromlist) 1 0.000 0.000 0.195 0.195 wand_extractor.py:1(<module>) 1668 0.005 0.000 0.190 0.000 re.py:289(_compile) 1 0.000 0.000 0.188 0.188 image.py:1(<module>)
Etsimme tulosteesta kohtaa missä näemme selvän erotuksen kumulatiivisessa suoritusajassa (cumtime). Tässä tapauksessa huomaamme harvinaisen selkeästi lähes kaiken ajan kuluvan säännöllisten lausekkeiden tulkintaan Pythonin re-kirjastossa. Kumulatiivisen suoritusajan mukaisessa järjestyksessä voimme myös seurata ketjua ylöspäin ja nähdä, että metodien kutsumajärjestys on käytännössä:
scraper -> detectors -> fido -> re
FIDO-työkalun analysointi
FIDO-työkalu toimii etsimällä tiedostoista PRONOM-rekisterissä määriteltyjä tunnisteita (eng. signatures). Tunnisteet ovat säännöllisiä lausekkeita, jotka etsivät tiedostomuodoista niille tyypillisiä piirteitä. Esimerkiksi DPX-kuvien tapauksessa PRONOM määrittelee:
Description Magic number in first four bytes: XPDS indicates little-endian
byte order; 4 byte offset to image; Version 1.0
Position type Absolute from BOF
Offset 0
Maximum Offset 0
Byte order
Value 58504453{4}56312E30
Tämä tarkoittaa, että kaikki version 1.0 little-endian (nouseva tavujärjestys) DPX-kuvatiedostot alkavat kaavan 58504453{4}56312E30 mukaisesti.
- 58504453: Tarkat neljä tavua (XPDS)
- {4}: Mielivaltaiset neljä tavua
- 56312E30: Tarkat neljä tavua (v1.0)
Voimme verrata tätä DPX-kuviimme ja näemme niiden seuraavan samaa kaavaa:
$ xxd kirkas.dpx | head -1 00000000: 5850 4453 8006 0000 5631 2e30 0000 0000 XPDS....V1.0....
FIDO etsii tiedostoista juuri näitä tunnisteita. Voimme muokata FIDO-lähdekoodia pitämään kirjaa jokaisen tunnisteen etsimisen suoritusnopeudesta. Tämä onnistuu seuraavilla muutoksilla FIDO v1.6.1 lähdekoodiin:
diff --git a/fido/fido.py b/fido/fido.py index 3b5bc60..e1a337d 100755 --- a/fido/fido.py +++ b/fido/fido.py @@ -693,11 +693,14 @@ class Fido: """ self.current_count += 1 result = [] + times = [] + import time for format in self.formats: try: self.current_format = format if self.as_good_as_any(format, result): for sig in self.get_signatures(format): + time_start = time.time() self.current_sig = sig success = True for pat in self.get_patterns(sig): @@ -723,6 +726,8 @@ class Fido: break if success: result.append((format, sig.findtext("name"))) + time_end = time.time() + times.append((sig, time_end-time_start)) except Exception as e: sys.stderr.write(str(e) + "\n") continue @@ -731,6 +736,10 @@ class Fido: # sys.stdout.write('***', self.get_puid(format), regex) result = [match for match in result if self.as_good_as_any(match[0], result)] + + for i in times: + print(f"{i[1]:.8f}\t{i[0].findtext('name')}") + return result def match_extensions(self, filename):
Suorittamalla ylläolevan koodin ja järjestämällä tulosteen löydämme kymmenen hitainta tiedostomuototunnistetta:
$ fido kirkas.dpx 2>/dev/null | sort -g | tail -10 0.00032163 Drawing Interchange File Format 1.2 0.00033593 Drawing Interchange File Format 2.2 0.00037837 Drawing Interchange File Format 1.0 0.00038409 Interlis transfer file 2.2 b 0.00039887 Interlis 2.3 Transfer File 2 0.00049639 BibTeX Database File 0.00053000 HFS 0.00067973 Apple Partition Map ISO 9660 Hybrid, sig n.1 0.00070667 Apple Partition Map ISO 9660 Hybrid, sig n.2 133.29057360 MPEG-1/2 Audio Layer 3
Käytännössä koko suoritusaika kuluu MP3-audiotiedoston tunnisteiden etsimiseen. Toistamalla saman tummalle kuvalle kahden minuutin sijaan samojen tunnisteiden etsimiseen kuluu 0.1 millisekuntia, mikä on noin miljoona kertaa tai kuusi kertaluokkaa nopeampaa.
$ fido tumma.dpx 2>/dev/null | grep 'MPEG-1/2' 0.00017285 MPEG-1/2 Audio Layer 3
Palapelin kasaaminen
Tässä vaiheessa nokkelimmat lukijat saattavat jo aavistaa mikä hitauden aiheuttaa. Olemme havainneet:
- Hitauden aiheuttavan FIDO-työkalun etsimät PRONOM-tunnisteet.
- MPEG-1/2 Audio Layer 3 -tunnisteen olevan ylivoimaisesti hitain etsiä: https://www.nationalarchives.gov.uk/pronom/fmt/134.
- Kuvan kirkkauden todellakin vaikuttavan ohjelmiston toimintaan, koska muuten identtinen tummempi kuva ei toista virhettä.
Tutkitaan seuraavaksi FIDO:n ensimmäisenä etsimää MP3-tunnistetta.
<signature> <name>MPEG-1 Audio Layer 3 with ID3v2 Tag</name> <note>Regularly-spaced frame headers should always be discoverable near EOF. An ID3v1 tag of up to 355 bytes may be present at EOF.</note> <pattern> <position>EOF</position> <pronom_pattern>FFFB[10:EB]{46-1439}FFFB[10:EB]{46-1439}FFFB[10:EB]{46-1439}FFFB[10:EB]{46-1439}FFFB[10:EB]{46-1439}FFFB[10:EB]{46-1439}FFFB[10:EB]{47-1795}</pronom_pattern> <regex>(?s)\xff\xfb[\x10-\xeb].{46,1439}\xff\xfb[\x10-\xeb].{46,1439}\xff\xfb[\x10-\xeb].{46,1439}\xff\xfb[\x10-\xeb].{46,1439}\xff\xfb[\x10-\xeb].{46,1439}\xff\xfb[\x10-\xeb].{46,1439}\xff\xfb[\x10-\xeb].{47,1795}\Z</regex> </pattern> <pattern> <position>BOF</position> <pronom_pattern>494433</pronom_pattern> <regex>(?s)\AID3</regex> </pattern> </signature>
Oleellinen osuus on regex-elementti, jota FIDO käyttää etsintään. Kyseinen säännöllinen lauseke etsii MP3-kehyksiä tiedoston lopusta. Yksittäinen tunnisteen etsimä kehys seuraa kaavaa:
fffb[10-eb].{46,1439}
- fffb: Tarkat kaksi tavua
- [10-eb]: Yksi mielivaltainen tavu 0x10 ja 0xeb väliltä
- .{46,1439}: 46-1439 mielivaltaista tavua
Kun vertaamme tätä kahteen DPX-kuvaamme:
$ xxd kirkas.dpx | tail -1 017bb670: ebff fbeb fffb ebff fbeb fffb ebff fbeb ................
$ xxd tumma.dpx | tail -1 017bb670: 1400 0414 0004 1400 0414 0004 1400 0414 ................
huomaamme kirkkaan kuvan noudattavan MP3-tunnisteen alkuosaa. Kirkkaan kuvan jokainen pikseli on mahdollinen MP3-kehyksen alku ja FIDO:n täytyy etsiä eteenpäin vastaako loputkin tiedostosta MP3-tunnistetta. Tämä tapahtuu aina kolmen tavun välein. Tumma kuva puolestaan ei osu ollenkaan tähän säännölliseen lausekkeeseen.
Voimme verrata valitsemaamme väriä fffbeb MP3-kehyksen määrityksiin:
AAAAAAAA AAABBCCD EEEEFFGH
11111111 11111011 11101011
ff fb eb
mikä vastaisi MP3-kehystä:
- MPEG Version 1 (ISO/IEC 11172-3)
- Layer III
- Not protected by CRC
- Bitrate 320 kbps
- Sampling rate 32000 Hz
- Frame is padded with one extra slot
- Arbitrary private bit is set.
MP3-tiedostomuodon määritykset selittävät myös kuvien kirkkauden vaikutuksen koodin suoritusaikaan. Määrityksien mukaan MP3-kehyksen ensimmäiset 11 bittiä ovat aina päällä, minkä takia kirkkailla kuvilla on suurempi todennäköisyys osittain osua tähän säännölliseen lausekkeeseen. Tämä myös tarkoittaa, että täysin valkoinen ffffff ei toista virhettä. Virheen toistaa vain tietyt kirkkaat värisävyt, jotka osuvat osittain MP3-tunnisteen säännölliseen lausekkeeseen. Virheen toistavat värit fffb10-fffbeb kartoittuvat aurinkoiseksi kuvaksi:

Yhteenveto
Tutkimme mielenkiintoista virhettä missä tiedostomuodon tunnistus oli kolme kertaluokkaa hitaampaa kirkkailla DPX-kuvilla kuin tummilla DPX-kuvilla. Virheen aiheutti DPX-kuvan tarkka värisävy mikä muistutti MP3-tiedostojen PRONOM-tunnistetta. Kuvan jokainen pikseli vastasi MP3-kehyksen formaattia ja teki säännöllisten lausekkeiden suorittamisesta todella hidasta.
Tämän ymmärtämisen jälkeen varsinainen korjaus on triviaali. Voimme siirtää MP3-tunnisteet viimeisiksi ja etsiä niitä ainoastaan, jos emme ole jo tunnistaneet tiedostoa DPX-kuvaksi. Tämä korjaa DPX-tiedostojen käsittelyn rikkomatta MP3-tiedostojen tunnistamista, mutta mahdollisesti jättää perään vastaavia reunatapauksia muilla pakkaamattomilla tiedostomuodoilla.
Vastaavat ongelmat säännöllisten lausekkeiden kanssa eivät ole ennenkuulumattomia. Yksi laajalti tunnettu tapaus on Cloudflaren palvelukatko vuodelta 2019. Säännölliset lausekkeet mahdollistavat monimutkaisen etsintälogiikan pienellä vaivalla, mutta voivat olla vaikeasti ymmärrettäviä ja virhealttiita. Säännöllisiä lausekkeita ei voi aina välttää, mutta on hyvän käytännön mukaista yrittää ennen niihin turvautumista käyttää muita keinoja.
Juho Kuisma
0x5527DB198DF3508A