diff options
-rw-r--r-- | .ghci | 7 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | bittorrent.cabal | 57 | ||||
-rw-r--r-- | src/System/IO/MMap/Fixed.hs | 203 | ||||
-rw-r--r-- | tests/MMap.hs | 45 |
6 files changed, 301 insertions, 16 deletions
@@ -1,5 +1,10 @@ | |||
1 | import Data.Serialize as S | 1 | import Data.Serialize as S |
2 | |||
2 | import Network | 3 | import Network |
3 | import Network.Socket hiding (send, sendTo, recv, recvFrom) | 4 | import Network.Socket hiding (send, sendTo, recv, recvFrom) |
4 | import Network.Socket.ByteString | 5 | import Network.Socket.ByteString |
5 | import Network.Torrent \ No newline at end of file | 6 | |
7 | import Network.Torrent | ||
8 | |||
9 | import System.IO.MMap.Fixed as F | ||
10 | import System.IO.MMap.Fixed as Fixed \ No newline at end of file | ||
@@ -1,5 +1,6 @@ | |||
1 | dist | 1 | dist |
2 | cabal-dev | 2 | cabal-dev |
3 | tmp | ||
3 | *.aux | 4 | *.aux |
4 | *.eventlog | 5 | *.eventlog |
5 | *.hp | 6 | *.hp |
diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b5c055dc --- /dev/null +++ b/Makefile | |||
@@ -0,0 +1,4 @@ | |||
1 | .PHONY: clean | ||
2 | |||
3 | clean: | ||
4 | rm -r tmp \ No newline at end of file | ||
diff --git a/bittorrent.cabal b/bittorrent.cabal index e7b2e33d..f99405fa 100644 --- a/bittorrent.cabal +++ b/bittorrent.cabal | |||
@@ -42,6 +42,7 @@ library | |||
42 | , Network.BitTorrent.Exchange | 42 | , Network.BitTorrent.Exchange |
43 | 43 | ||
44 | other-modules: Network.BitTorrent.Internal | 44 | other-modules: Network.BitTorrent.Internal |
45 | , System.IO.MMap.Fixed | ||
45 | 46 | ||
46 | if flag(testing) | 47 | if flag(testing) |
47 | exposed-modules: Network.BitTorrent.Exchange.Protocol | 48 | exposed-modules: Network.BitTorrent.Exchange.Protocol |
@@ -56,12 +57,12 @@ library | |||
56 | build-depends: | 57 | build-depends: |
57 | base == 4.* | 58 | base == 4.* |
58 | 59 | ||
59 | -- Control packages | 60 | -- Control |
60 | , mtl | 61 | , mtl |
61 | , resourcet | 62 | , resourcet |
62 | , lens | 63 | , lens |
63 | 64 | ||
64 | -- Concurrency packages | 65 | -- Concurrency |
65 | , SafeSemaphore | 66 | , SafeSemaphore |
66 | , BoundedChan >= 1.0.1.0 | 67 | , BoundedChan >= 1.0.1.0 |
67 | , stm >= 2.4 | 68 | , stm >= 2.4 |
@@ -71,50 +72,58 @@ library | |||
71 | , network-conduit == 1.* | 72 | , network-conduit == 1.* |
72 | , cereal-conduit >= 0.5 | 73 | , cereal-conduit >= 0.5 |
73 | 74 | ||
74 | -- Data packages | 75 | -- Data |
75 | , array >= 0.4 | 76 | , array >= 0.4 |
76 | , bytestring | 77 | , bytestring >= 0.10 |
77 | -- >= 0.10.2 | 78 | |
78 | , containers >= 0.4 | 79 | , containers >= 0.4 |
79 | , intset >= 0.1 | 80 | , intset >= 0.1 |
81 | , IntervalMap >= 0.3 | ||
82 | |||
80 | , text >= 0.11.0 | 83 | , text >= 0.11.0 |
81 | , vector | 84 | , vector |
82 | 85 | ||
83 | -- Encoding/Serialization packages | 86 | -- Encoding/Serialization |
84 | , bencoding >= 0.1 | 87 | , bencoding >= 0.1 |
85 | , cereal >= 0.3 | 88 | , cereal >= 0.3 |
86 | , urlencoded >= 0.4 | 89 | , urlencoded >= 0.4 |
87 | 90 | ||
88 | -- Time packages | 91 | -- Time |
89 | , time >= 0.1 | 92 | , time >= 0.1 |
90 | , old-locale >= 1.0 | 93 | , old-locale >= 1.0 |
91 | 94 | ||
92 | -- Network packages | 95 | -- Network |
93 | , network >= 2.4 | 96 | , network >= 2.4 |
94 | , HTTP >= 4000.2 | 97 | , HTTP >= 4000.2 |
95 | , krpc | 98 | , krpc |
96 | 99 | ||
100 | -- System | ||
101 | , filepath >= 1 | ||
102 | , mmap >= 0.5.2 | ||
103 | |||
97 | -- Misc | 104 | -- Misc |
98 | , data-default | 105 | , data-default |
99 | , cryptohash | 106 | , cryptohash |
100 | , filepath >= 1 | ||
101 | , bits-atomic >= 0.1 | ||
102 | , pretty | 107 | , pretty |
103 | 108 | ||
109 | , bits-atomic >= 0.1 | ||
110 | |||
104 | extensions: PatternGuards | 111 | extensions: PatternGuards |
105 | hs-source-dirs: src | 112 | hs-source-dirs: src |
106 | if flag(testing) | 113 | if flag(testing) |
107 | cpp-options: -DTESTING | 114 | cpp-options: -DTESTING |
108 | ghc-options: -Wall | 115 | ghc-options: -Wall |
109 | 116 | ||
110 | executable exsample | 117 | |
118 | |||
119 | executable example | ||
111 | main-is: Main.hs | 120 | main-is: Main.hs |
112 | hs-source-dirs: exsamples | 121 | hs-source-dirs: examples |
113 | build-depends: base == 4.* | 122 | build-depends: base == 4.* |
114 | , bittorrent | 123 | , bittorrent |
115 | , mtl | 124 | , mtl |
116 | 125 | ||
117 | ghc-options: -O2 -rtsopts -threaded | 126 | ghc-options: -O2 -rtsopts -threaded |
118 | -- -threaded -rtsopts | 127 | -- -threaded -rtsopts |
119 | -- -eventlog | 128 | -- -eventlog |
120 | -- ghc-prof-options: -prof -auto-all -caf-all | 129 | -- ghc-prof-options: -prof -auto-all -caf-all |
@@ -123,6 +132,7 @@ executable exsample | |||
123 | buildable: False | 132 | buildable: False |
124 | 133 | ||
125 | 134 | ||
135 | |||
126 | test-suite info-hash | 136 | test-suite info-hash |
127 | type: exitcode-stdio-1.0 | 137 | type: exitcode-stdio-1.0 |
128 | main-is: InfoHash.hs | 138 | main-is: InfoHash.hs |
@@ -143,6 +153,23 @@ test-suite info-hash | |||
143 | 153 | ||
144 | 154 | ||
145 | 155 | ||
156 | test-suite mmap | ||
157 | type: exitcode-stdio-1.0 | ||
158 | main-is: MMap.hs | ||
159 | build-depends: base == 4.* | ||
160 | , bytestring | ||
161 | , directory | ||
162 | |||
163 | , HUnit | ||
164 | , test-framework | ||
165 | , test-framework-hunit | ||
166 | |||
167 | , mmap-fixed | ||
168 | |||
169 | hs-source-dirs: tests | ||
170 | |||
171 | |||
172 | |||
146 | test-suite properties | 173 | test-suite properties |
147 | type: exitcode-stdio-1.0 | 174 | type: exitcode-stdio-1.0 |
148 | main-is: Main.hs | 175 | main-is: Main.hs |
diff --git a/src/System/IO/MMap/Fixed.hs b/src/System/IO/MMap/Fixed.hs new file mode 100644 index 00000000..9d2e354f --- /dev/null +++ b/src/System/IO/MMap/Fixed.hs | |||
@@ -0,0 +1,203 @@ | |||
1 | -- TODO pprint | ||
2 | -- TODO see if this IntervalMap is overkill: Interval dataty have 4 constrs | ||
3 | -- TODO clarify lifetime in docs | ||
4 | -- TODO use madvise | ||
5 | -- TODO unmap selected interval | ||
6 | -- TODO tests | ||
7 | -- TODO benchmarks | ||
8 | -- TODO unmap overlapped regions | ||
9 | -- [A] TODO lazy mapping for 32 bit arch; | ||
10 | -- we need tricky algorithm and a lot of perf tests | ||
11 | -- TODO use memmove in write bytes | ||
12 | -- TODO write elem, write byte, read byte | ||
13 | -- | | ||
14 | -- Copyright : (c) Sam T. 2013 | ||
15 | -- License : MIT | ||
16 | -- Maintainer : pxqr.sta@gmail.com | ||
17 | -- Stability : experimental | ||
18 | -- Portability : portable | ||
19 | -- | ||
20 | -- This library provides mechanism to mmap files to fixed address | ||
21 | -- with fine-grained control. Hovewer, instead of using MAP_FIXED we | ||
22 | -- create our own address space upon virtual address space. If you | ||
23 | -- would like you could call this space as "fixed address space". | ||
24 | -- | ||
25 | -- This solves a few problems: | ||
26 | -- | ||
27 | -- * Page already in use. If you mmap one file at 0..x addresses and | ||
28 | -- want to map second file to x..y addresses using MAP_FIXED you | ||
29 | -- can get in troubles: page might be mapped already. Raw call to | ||
30 | -- mmap will silently unmap x..y addresses and then mmap our second | ||
31 | -- file. So here we have extra unmap we would like to avoid. | ||
32 | -- | ||
33 | -- * Page boundaries. If you mmap one file at x..x+1 you could | ||
34 | -- not map next file to say addresses x+1..x+2. | ||
35 | -- | ||
36 | -- Internally we make ordinary call to mmap to map a file and then | ||
37 | -- using /interval map/ we map fixed address space to virtual | ||
38 | -- address space. It takes TODO time in TODO cases. | ||
39 | -- | ||
40 | -- Basically this library could be used when we need coalesce | ||
41 | -- several files in arbitrary way. We could map at any position as | ||
42 | -- long as offset + size fit in 'Int'. | ||
43 | -- | ||
44 | -- For other details see: | ||
45 | -- | ||
46 | -- > http://hackage.haskell.org/package/mmap | ||
47 | -- > man mmap | ||
48 | -- | ||
49 | module System.IO.MMap.Fixed | ||
50 | ( -- * Intervals | ||
51 | FixedOffset, FileOffset, FixedInterval, FileInterval | ||
52 | , interval, fileInterval | ||
53 | |||
54 | -- * Construction | ||
55 | , Fixed, Bytes | ||
56 | , System.IO.MMap.Fixed.empty, insertTo | ||
57 | , coalesceFiles | ||
58 | |||
59 | -- ** Specialized 'insertTo' | ||
60 | , mmapTo, mallocTo | ||
61 | |||
62 | -- * Query | ||
63 | , upperAddr | ||
64 | |||
65 | -- * Access | ||
66 | , viewBytes, readBytes, writeBytes | ||
67 | , readElem, writeElem | ||
68 | ) where | ||
69 | |||
70 | import Data.ByteString.Lazy as Lazy | ||
71 | import Data.ByteString.Lazy.Internal as Lazy | ||
72 | import Data.ByteString.Internal as B | ||
73 | import Data.List as L | ||
74 | import Data.Int | ||
75 | import Data.IntervalMap.Strict as M | ||
76 | import Data.IntervalMap.Interval | ||
77 | import System.IO.MMap | ||
78 | import Foreign | ||
79 | |||
80 | |||
81 | type FixedOffset = Int | ||
82 | type FileOffset = Int64 | ||
83 | type Size = Int | ||
84 | |||
85 | |||
86 | type FileInterval = (FileOffset, Size) | ||
87 | type FixedInterval = Interval FixedOffset | ||
88 | |||
89 | |||
90 | interval :: FixedOffset -> Size -> FixedInterval | ||
91 | interval off s = IntervalCO off (off + fromIntegral (max 0 s)) | ||
92 | {-# INLINE interval #-} | ||
93 | |||
94 | fileInterval :: FileOffset -> Size -> FileInterval | ||
95 | fileInterval off s = (off, s) | ||
96 | {-# INLINE fileInterval #-} | ||
97 | |||
98 | intervalSize :: FixedInterval -> Size | ||
99 | intervalSize i = upperBound i - lowerBound i | ||
100 | {-# INLINE intervalSize #-} | ||
101 | |||
102 | |||
103 | type Bytes = (ForeignPtr Word8, Size) | ||
104 | |||
105 | type FixedMap = IntervalMap FixedOffset Bytes | ||
106 | |||
107 | newtype Fixed = Fixed { imap :: FixedMap } | ||
108 | |||
109 | instance Show Fixed where | ||
110 | show = show . M.toList . imap | ||
111 | |||
112 | |||
113 | mapIM :: (FixedMap -> FixedMap) -> Fixed -> Fixed | ||
114 | mapIM f s = s { imap = f (imap s) } | ||
115 | |||
116 | empty :: Fixed | ||
117 | empty = Fixed M.empty | ||
118 | |||
119 | coalesceFiles :: [(FilePath, Int)] -> IO Fixed | ||
120 | coalesceFiles = go 0 System.IO.MMap.Fixed.empty | ||
121 | where | ||
122 | go _ s [] = return s | ||
123 | go offset s ((path, bsize) : xs) = do | ||
124 | s' <- mmapTo path (0, bsize) offset s | ||
125 | go (offset + bsize) s' xs | ||
126 | |||
127 | upperAddr :: Fixed -> FixedOffset | ||
128 | upperAddr = upperBound . fst . findLast . imap | ||
129 | |||
130 | insertTo :: FixedInterval -> Bytes -> Fixed -> Fixed | ||
131 | insertTo fi mm = mapIM (M.insert fi mm) | ||
132 | {-# INLINE insertTo #-} | ||
133 | |||
134 | mmapTo :: FilePath -> FileInterval -> FixedOffset -> Fixed -> IO Fixed | ||
135 | mmapTo path mrange to s = do | ||
136 | (fptr, offset, fsize) <- mmapFileForeignPtr path ReadWriteEx (Just mrange) | ||
137 | |||
138 | let fixed = interval to fsize | ||
139 | let mmaped = (fptr, offset) | ||
140 | |||
141 | return $ insertTo fixed mmaped s | ||
142 | |||
143 | mallocTo :: FixedInterval -> Fixed -> IO Fixed | ||
144 | mallocTo fi s = do | ||
145 | let bsize = intervalSize fi | ||
146 | fptr <- mallocForeignPtrBytes bsize | ||
147 | return (insertTo fi (fptr, 0) s) | ||
148 | |||
149 | -- | Note: this is unsafe operation. | ||
150 | viewBytes :: FixedInterval -> Fixed -> Lazy.ByteString | ||
151 | viewBytes fi s = fromChunks $ L.map mk $ (imap s `intersecting` fi) | ||
152 | where | ||
153 | mk (i, (fptr, offset)) = | ||
154 | let dropB = max 0 (lowerBound fi - lowerBound i) | ||
155 | dropT = max 0 (upperBound i - upperBound fi) | ||
156 | bsize = intervalSize i - (dropT + dropB) | ||
157 | in fromForeignPtr fptr (offset + dropB) bsize | ||
158 | |||
159 | |||
160 | readBytes :: FixedInterval -> Fixed -> IO Lazy.ByteString | ||
161 | readBytes fi s = let c = Lazy.copy (viewBytes fi s) in mkCopy c >> return c | ||
162 | {-# INLINE readBytes #-} | ||
163 | |||
164 | writeBytes :: FixedInterval -> Lazy.ByteString -> Fixed -> IO () | ||
165 | writeBytes fi bs s = bscpy (viewBytes fi s) bs | ||
166 | {-# INLINE writeBytes #-} | ||
167 | |||
168 | -- | Note: this operation takes O(log(files count)) time, if possible | ||
169 | -- use readBytes. | ||
170 | readElem :: Storable a => Fixed -> FixedOffset -> IO a | ||
171 | readElem s offset = go undefined | ||
172 | where | ||
173 | go :: Storable a => a -> IO a | ||
174 | go dont_touch = do | ||
175 | let bsize = sizeOf dont_touch | ||
176 | let PS fptr off _ = Lazy.toStrict (viewBytes (interval offset bsize) s) | ||
177 | withForeignPtr fptr $ \ ptr -> peekByteOff ptr off | ||
178 | |||
179 | writeElem :: Storable a => Fixed -> FixedOffset -> a -> IO () | ||
180 | writeElem s offset x = do | ||
181 | let bsize = sizeOf x | ||
182 | let PS fptr off _ = Lazy.toStrict (viewBytes (interval offset bsize) s) | ||
183 | withForeignPtr fptr $ \ptr -> pokeByteOff ptr off x | ||
184 | |||
185 | |||
186 | mkCopy :: Lazy.ByteString -> IO () | ||
187 | mkCopy Empty = return () | ||
188 | mkCopy (Chunk _ x) = mkCopy x | ||
189 | |||
190 | bscpy :: Lazy.ByteString -> Lazy.ByteString -> IO () | ||
191 | bscpy (PS _ _ 0 `Chunk` dest_rest) src = bscpy dest_rest src | ||
192 | bscpy dest (PS _ _ 0 `Chunk` src_rest) = bscpy dest src_rest | ||
193 | bscpy (PS dest_fptr dest_off dest_size `Chunk` dest_rest) | ||
194 | (PS src_fptr src_off src_size `Chunk` src_rest) | ||
195 | = do let csize = min dest_size src_size | ||
196 | withForeignPtr dest_fptr $ \dest_ptr -> | ||
197 | withForeignPtr src_fptr $ \src_ptr -> | ||
198 | memcpy (dest_ptr `advancePtr` dest_off) | ||
199 | (src_ptr `advancePtr` src_off) | ||
200 | (fromIntegral csize) -- TODO memmove? | ||
201 | bscpy (PS dest_fptr (dest_off + csize) (dest_size - csize) `Chunk` dest_rest) | ||
202 | (PS src_fptr (src_off + csize) (src_size - csize) `Chunk` src_rest) | ||
203 | bscpy _ _ = return () \ No newline at end of file | ||
diff --git a/tests/MMap.hs b/tests/MMap.hs new file mode 100644 index 00000000..a4be95f0 --- /dev/null +++ b/tests/MMap.hs | |||
@@ -0,0 +1,45 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | module Main (main) where | ||
3 | |||
4 | import Test.HUnit | ||
5 | import Test.Framework | ||
6 | import Test.Framework.Providers.HUnit | ||
7 | import Data.Word | ||
8 | |||
9 | import System.Directory | ||
10 | import System.IO.MMap.Fixed | ||
11 | |||
12 | |||
13 | boundaryTest = do | ||
14 | f <- mallocTo (interval 0 1) empty | ||
15 | f <- mallocTo (interval 1 2) f | ||
16 | writeElem f 0 (1 :: Word8) | ||
17 | writeElem f 1 (2 :: Word8) | ||
18 | bs <- readBytes (interval 0 2) f | ||
19 | "\x1\x2" @=? bs | ||
20 | |||
21 | mmapSingle = do | ||
22 | f <- mmapTo "single.test" (10, 5) 5 empty | ||
23 | writeBytes (interval 5 5) "abcde" f | ||
24 | bs <- readBytes (interval 5 5) f | ||
25 | "abcde" @=? bs | ||
26 | |||
27 | coalesceTest = do | ||
28 | f <- mmapTo "a.test" (0, 1) 10 empty | ||
29 | f <- mmapTo "bc.test" (0, 2) 12 f | ||
30 | f <- mmapTo "c.test" (0, 1) 13 f | ||
31 | writeBytes (interval 10 4) "abcd" f | ||
32 | bs <- readBytes (interval 10 4) f | ||
33 | "abcd" @=? bs | ||
34 | |||
35 | main :: IO () | ||
36 | main = do | ||
37 | let tmpdir = "tmp" | ||
38 | createDirectoryIfMissing True tmpdir | ||
39 | setCurrentDirectory tmpdir | ||
40 | |||
41 | defaultMain | ||
42 | [ testCase "boudary" boundaryTest | ||
43 | , testCase "single" mmapSingle | ||
44 | , testCase "coalesce" coalesceTest | ||
45 | ] \ No newline at end of file | ||