Repository: python-pillow/Pillow

Issue:
PyPy performance on test_image_point is awful
Hoisted from #476, test_image_point.py takes ~ 2 minutes to run, vs < 1 sec for cpython.



Discussion:
Can this be closed?

Reported to PyPy.

https://bitbucket.org/pypy/pypy/issue/1779/impoint-4000x-slower-than-cpython

PyPy say this is fixed as of 2014-07-02.

The latest 2.3.1 was released before this (and is slated for Travis later this week), but there are nightly build available.

Doesn't look like it. For comparison, py27 runs this in .2 seconds under the profiler. 

```
(vpypy)erics@builder-1204-x64:~/Pillow$ python --version
Python 2.7.6 (2.3.1+dfsg-1~ppa1, Jun 20 2014, 09:27:47)
[PyPy 2.3.1 with GCC 4.6.3]
(vpypy)erics@builder-1204-x64:~/Pillow$ NOSE_PROCESSES=0 ./profile-installed.py Tests/test_image_point.py > pypy_imagepoint.out
..
----------------------------------------------------------------------
Ran 2 tests in 132.968s

OK
(vpypy)erics@builder-1204-x64:~/Pillow$ more pypy_imagepoint.out
         70974 function calls (68708 primitive calls) in 132.472 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000  132.472  132.472 profile:0(nose.main())
        1    0.000    0.000  132.472  132.472 core.py:102(__init__)
     20/1    0.000    0.000  132.472  132.472 <string>:1(<module>)
        1    0.000    0.000  132.260  132.260 main.py:72(__init__)
        1    0.000    0.000  131.328  131.328 core.py:195(runTests)
        1    0.000    0.000  131.328  131.328 core.py:46(run)
      4/1    0.004    0.001  131.328  131.328 suite.py:196(run)
      4/1    0.000    0.000  131.328  131.328 suite.py:175(__call__)
        2    0.000    0.000  131.316   65.658 case.py:44(__call__)
        2    0.000    0.000  131.316   65.658 case.py:115(run)
        2    0.000    0.000  131.316   65.658 case.py:297(run)
        2    0.000    0.000  131.316   65.658 case.py:394(__call__)
        2    0.000    0.000  131.316   65.658 helper.py:26(run)
        2    0.000    0.000  131.316   65.658 case.py:142(runTest)
        1    0.000    0.000  131.276  131.276 test_image_point.py:29(test_16bit_lut)
       10  129.640   12.964  129.652   12.965 Image.py:1321(point)
      3/2    0.040    0.013    1.660    0.830 helper.py:185(lena)
       88    0.020    0.000    1.544    0.018 re.py:226(_compile)
       33    0.000    0.000    1.536    0.047 re.py:188(compile)
       32    0.008    0.000    1.516    0.047 sre_compile.py:493(compile)
....
```

This profiler script is my new favorite toy. Though I'm going to have to profile the C to figure out where the bottleneck really is, since it's more than likely in the im.point call on line Image.py:1360: `return self._new(self.im.point(lut, mode))`.

bah, I missed that the fix missed 2.3.1


Resolving patch:
diff --git a/profile-installed.py b/profile-installed.py
--- a/profile-installed.py
+++ b/profile-installed.py
@@ -21,14 +21,6 @@
 # Make sure that nose doesn't muck with our paths.
 if ('--no-path-adjustment' not in sys.argv) and ('-P' not in sys.argv):
     sys.argv.insert(1, '--no-path-adjustment')
-
-if 'NOSE_PROCESSES' not in os.environ:
-    for arg in sys.argv:
-        if '--processes' in arg:
-            break
-    else: #  for
-        sys.argv.insert(1, '--processes=-1') # -1 == number of cores
-        sys.argv.insert(1, '--process-timeout=30') 
     
 if __name__ == '__main__':
     profile.run("nose.main()", sort=2)


Test patch:
diff --git a/Tests/helper.py b/Tests/helper.py
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -124,7 +124,8 @@ def assert_warning(self, warn_class, func):
             self.assertTrue(found)
         return result
 
-    def skipKnownBadTest(self, msg=None, platform=None, travis=None):
+    def skipKnownBadTest(self, msg=None, platform=None,
+                         travis=None, interpreter=None):
         # Skip if platform/travis matches, and
         # PILLOW_RUN_KNOWN_BAD is not true in the environment.
         if bool(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)):
@@ -136,6 +137,8 @@ def skipKnownBadTest(self, msg=None, platform=None, travis=None):
             skip = sys.platform.startswith(platform)
         if travis is not None:
             skip = skip and (travis == bool(os.environ.get('TRAVIS', False)))
+        if interpreter is not None:
+            skip = skip and (interpreter == 'pypy' and hasattr(sys, 'pypy_version_info'))
         if skip:
             self.skipTest(msg or "Known Bad Test")
 
diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -5,12 +5,6 @@
 
 class TestImagePoint(PillowTestCase):
 
-    def setUp(self):
-        if hasattr(sys, 'pypy_version_info'):
-            # This takes _forever_ on PyPy. Open Bug,
-            # see https://github.com/python-pillow/Pillow/issues/484
-            self.skipTest("Too slow on PyPy")
-
     def test_sanity(self):
         im = lena()
 
@@ -29,11 +23,24 @@ def test_sanity(self):
     def test_16bit_lut(self):
         """ Tests for 16 bit -> 8 bit lut for converting I->L images
             see https://github.com/python-pillow/Pillow/issues/440
-        """
+            """
+        # This takes _forever_ on PyPy. Open Bug,
+        # see https://github.com/python-pillow/Pillow/issues/484
+        #self.skipKnownBadTest(msg="Too Slow on pypy", interpreter='pypy')
 
         im = lena("I")
         im.point(list(range(256))*256, 'L')
 
+    def test_f_lut(self):
+        """ Tests for floating point lut of 8bit gray image """
+        im = lena('L')
+        lut = [0.5 * float(x) for x in range(256)]
+
+        out = im.point(lut, 'F')
+
+        int_lut = [x//2 for x in range(256)]
+        self.assert_image_equal(out.convert('L'), im.point(int_lut, 'L'))
+        
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -42,6 +42,30 @@ def put(value):
             self.assertEqual(put(sys.maxsize), (255, 255, 255, 127))
 
 
+    def test_pypy_performance(self):
+        im = Image.new('L', (256,256))
+        im.putdata(list(range(256))*256)
+
+    def test_mode_i(self):
+        src = lena('L')
+        data = list(src.getdata())
+        im = Image.new('I', src.size, 0)
+        im.putdata(data, 2, 256)
+
+        target = [2* elt + 256 for elt in data]
+        self.assertEqual(list(im.getdata()), target)
+
+    def test_mode_F(self):
+        src = lena('L')
+        data = list(src.getdata())
+        im = Image.new('F', src.size, 0)
+        im.putdata(data, 2.0, 256.0)
+
+        target = [2.0* float(elt) + 256.0 for elt in data]
+        self.assertEqual(list(im.getdata()), target)
+
+    
+
 if __name__ == '__main__':
     unittest.main()
 
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -5,18 +5,22 @@
 
 class TestModeI16(PillowTestCase):
 
+    original = lena().resize((32,32)).convert('I')
+
     def verify(self, im1):
-        im2 = lena("I")
+        im2 = self.original.copy()
         self.assertEqual(im1.size, im2.size)
         pix1 = im1.load()
         pix2 = im2.load()
         for y in range(im1.size[1]):
             for x in range(im1.size[0]):
                 xy = x, y
+                p1 = pix1[xy]
+                p2 = pix2[xy]
                 self.assertEqual(
-                    pix1[xy], pix2[xy],
+                    p1, p2,
                     ("got %r from mode %s at %s, expected %r" %
-                        (pix1[xy], im1.mode, xy, pix2[xy])))
+                        (p1, im1.mode, xy, p2)))
 
     def test_basic(self):
         # PIL 1.1 has limited support for 16-bit image data.  Check that
@@ -24,7 +28,7 @@ def test_basic(self):
 
         def basic(mode):
 
-            imIn = lena("I").convert(mode)
+            imIn = self.original.convert(mode)
             self.verify(imIn)
 
             w, h = imIn.size
@@ -92,7 +96,7 @@ def tobytes(mode):
 
     def test_convert(self):
 
-        im = lena("I")
+        im = self.original.copy()
 
         self.verify(im.convert("I;16"))
         self.verify(im.convert("I;16").convert("L"))